diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 249bcaf28f1..dd005f98b74 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -54,6 +54,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + build-ubuntu: name: Build ubuntu24 (JDK 17 / aarch64) if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'ubuntu' }} @@ -84,6 +93,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + docker-build-rockylinux: name: Build rockylinux (JDK 8 / x86_64) if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'rockylinux' }} @@ -127,6 +145,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + - name: Test with RocksDB engine run: ./gradlew :framework:testWithRocksDb --no-daemon @@ -172,6 +199,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon --no-build-cache + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + - name: Test with RocksDB engine run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache 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 e7957c917e2..8979b5ed68e 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -338,7 +338,7 @@ public class CommonParameter { @Getter @Setter - public boolean allowShieldedTransactionApi; // clearParam: true + public boolean allowShieldedTransactionApi; // clearParam: false @Getter @Setter public long blockNumForEnergyLimit; diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index f9cf05a917f..8f7a39e02c0 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -86,7 +86,7 @@ public class NodeConfig { private boolean unsolidifiedBlockCheck = false; private int maxUnsolidifiedBlocks = 54; private String zenTokenId = "000000"; - private boolean allowShieldedTransactionApi = true; + private boolean allowShieldedTransactionApi = false; private double activeConnectFactor = 0.1; private double connectFactor = 0.6; // Legacy alias `maxActiveNodesWithSameIp` has no bean field: we only peek at it via diff --git a/common/src/main/java/org/tron/core/exception/ZksnarkException.java b/common/src/main/java/org/tron/core/exception/ZksnarkException.java index ec75e03852b..fab8019aebf 100644 --- a/common/src/main/java/org/tron/core/exception/ZksnarkException.java +++ b/common/src/main/java/org/tron/core/exception/ZksnarkException.java @@ -9,4 +9,8 @@ public ZksnarkException() { public ZksnarkException(String message) { super(message); } + + public ZksnarkException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 07f2114d061..4f2e061a65f 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -215,7 +215,7 @@ node { minParticipationRate = 0 # Whether to enable shielded transaction API - allowShieldedTransactionApi = true + allowShieldedTransactionApi = false # Whether to print config log at startup openPrintLog = true diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index fb22029262e..a52c51c1ba4 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -284,26 +284,26 @@ public void testLegacyAliasTakesPriorityOverModernKey() { } @Test - public void testShieldedApiDefaultsToTrueWhenNeitherKeySet() { + public void testShieldedApiDefaultsToFalseWhenNeitherKeySet() { NodeConfig nc = NodeConfig.fromConfig(withRef()); - assertTrue(nc.isAllowShieldedTransactionApi()); + assertFalse(nc.isAllowShieldedTransactionApi()); } @Test public void testShieldedApiModernKeyRespected() { NodeConfig nc = NodeConfig.fromConfig( - withRef("node.allowShieldedTransactionApi = false")); - assertFalse(nc.isAllowShieldedTransactionApi()); + withRef("node.allowShieldedTransactionApi = true")); + assertTrue(nc.isAllowShieldedTransactionApi()); } @Test public void testShieldedApiLegacyKeyRespected() { - // Regression guard: reference.conf ships `allowShieldedTransactionApi = true`, which + // Regression guard: reference.conf ships `allowShieldedTransactionApi = false`, which // used to make the legacy-key fallback dead code. A user who only set the legacy key // must still have their value honored. NodeConfig nc = NodeConfig.fromConfig( - withRef("node.fullNodeAllowShieldedTransaction = false")); - assertFalse(nc.isAllowShieldedTransactionApi()); + withRef("node.fullNodeAllowShieldedTransaction = true")); + assertTrue(nc.isAllowShieldedTransactionApi()); } @Test diff --git a/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java b/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java new file mode 100644 index 00000000000..26fa8fdd99a --- /dev/null +++ b/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java @@ -0,0 +1,29 @@ +package org.tron.core.exception; + +import org.junit.Assert; +import org.junit.Test; + +public class ZksnarkExceptionTest { + + @Test + public void testNoArgConstructor() { + ZksnarkException e = new ZksnarkException(); + Assert.assertNull(e.getMessage()); + Assert.assertNull(e.getCause()); + } + + @Test + public void testMessageConstructor() { + ZksnarkException e = new ZksnarkException("boom"); + Assert.assertEquals("boom", e.getMessage()); + Assert.assertNull(e.getCause()); + } + + @Test + public void testMessageAndCauseConstructor() { + Throwable cause = new ArithmeticException("overflow"); + ZksnarkException e = new ZksnarkException("wrapped", cause); + Assert.assertEquals("wrapped", e.getMessage()); + Assert.assertSame(cause, e.getCause()); + } +} diff --git a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java index 48181cb1255..ceefa9a8cae 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java +++ b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java @@ -1,8 +1,7 @@ package org.tron.common.logsfilter; -import static org.tron.common.math.Maths.min; - import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; @@ -38,9 +37,14 @@ public static String parseDataBytes(byte[] data, String typeStr, int index) { byte[] lengthBytes = subBytes(data, start, DATAWORD_UNIT_SIZE); // this length is byte count. no need X 32 int length = intValueExact(lengthBytes); + if (length < 0) { + throw new OutputLengthException("data length:" + length); + } byte[] realBytes = length > 0 ? subBytes(data, start + DATAWORD_UNIT_SIZE, length) : new byte[0]; - return type == Type.STRING ? new String(realBytes) : Hex.toHexString(realBytes); + return type == Type.STRING + ? new String(realBytes, StandardCharsets.UTF_8) + : Hex.toHexString(realBytes); } } catch (OutputLengthException | ArithmeticException e) { logger.debug("parseDataBytes ", e); @@ -74,11 +78,15 @@ protected static Integer intValueExact(byte[] data) { } protected static byte[] subBytes(byte[] src, int start, int length) { - if (ArrayUtils.isEmpty(src) || start >= src.length || length < 0) { - throw new OutputLengthException("data start:" + start + ", length:" + length); + if (ArrayUtils.isEmpty(src)) { + throw new OutputLengthException("source data is empty"); + } + if (start < 0 || start >= src.length || length < 0 || length > src.length - start) { + throw new OutputLengthException( + "data start:" + start + ", length:" + length + ", src.length:" + src.length); } byte[] dst = new byte[length]; - System.arraycopy(src, start, dst, 0, min(length, src.length - start, true)); + System.arraycopy(src, start, dst, 0, length); return dst; } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 2c35fbd6447..ce3c3ac68f1 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -241,7 +241,6 @@ import org.tron.protos.contract.BalanceContract.BlockBalanceTrace; import org.tron.protos.contract.BalanceContract.TransferContract; import org.tron.protos.contract.Common; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleTree; import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; import org.tron.protos.contract.ShieldContract.OutputPoint; import org.tron.protos.contract.ShieldContract.OutputPointInfo; @@ -257,7 +256,9 @@ @Component public class Wallet { - private static final String SHIELDED_ID_NOT_ALLOWED = "ShieldedTransactionApi is not allowed"; + private static final String SHIELDED_ID_NOT_ALLOWED = + "Shielded transaction API is disabled; " + + "set node.allowShieldedTransactionApi=true to enable."; private static final String PAYMENT_ADDRESS_FORMAT_WRONG = "paymentAddress format is wrong"; private static final String SHIELDED_TRANSACTION_SCAN_RANGE = "request requires start_block_index >= 0 && end_block_index > " @@ -569,41 +570,41 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { return builder.setResult(true).setCode(response_code.SUCCESS).build(); } } catch (ValidateSignatureException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.SIGERROR) .setMessage(ByteString.copyFromUtf8("Validate signature error: " + e.getMessage())) .build(); } catch (ContractValidateException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR) .setMessage(ByteString.copyFromUtf8(CONTRACT_VALIDATE_ERROR + e.getMessage())) .build(); } catch (ContractExeException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR) .setMessage(ByteString.copyFromUtf8("Contract execute error : " + e.getMessage())) .build(); } catch (AccountResourceInsufficientException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.BANDWITH_ERROR) .setMessage(ByteString.copyFromUtf8("Account resource insufficient error.")) .build(); } catch (DupTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8("Dup transaction.")) .build(); } catch (TaposException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TAPOS_ERROR) .setMessage(ByteString.copyFromUtf8("Tapos check error.")) .build(); } catch (TooBigTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8(e.getMessage())).build(); } catch (TransactionExpirationException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR) .setMessage(ByteString.copyFromUtf8("Transaction expired")) .build(); @@ -747,7 +748,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, @@ -1469,8 +1470,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()) @@ -2179,23 +2180,6 @@ public IncrementalMerkleVoucherInfo getMerkleTreeVoucherInfo(OutputPointInfo req return result.build(); } - public IncrementalMerkleTree getMerkleTreeOfBlock(long blockNum) throws ZksnarkException { - checkAllowShieldedTransactionApi(); - if (blockNum < 0) { - return null; - } - - try { - if (chainBaseManager.getMerkleTreeIndexStore().has(ByteArray.fromLong(blockNum))) { - return IncrementalMerkleTree - .parseFrom(chainBaseManager.getMerkleTreeIndexStore().get(blockNum)); - } - } catch (Exception ex) { - logger.error("GetMerkleTreeOfBlock failed, blockNum:{}", blockNum, ex); - } - - return null; - } public long getShieldedTransactionFee() { return chainBaseManager.getDynamicPropertiesStore().getShieldedTransactionFee(); @@ -2292,58 +2276,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", e); } catch (ZksnarkException e) { - logger.error("createShieldedTransaction except, error is " + e.toString()); - throw new ZksnarkException(e.toString()); + logger.error("createShieldedTransaction except, error is {}", e.toString()); + throw e; } - return transactionCapsule; - } public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( @@ -2394,59 +2378,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", e); } catch (ZksnarkException e) { - logger.error("createShieldedTransaction exception, error is " + e.toString()); - throw new ZksnarkException(e.toString()); + logger.error("createShieldedTransaction exception, error is {}", e.toString()); + throw e; } - return transactionCapsule; - } private void shieldedOutput(List shieldedReceives, @@ -2464,7 +2449,6 @@ private void shieldedOutput(List shieldedReceives, } } - public ShieldedAddressInfo getNewShieldedAddress() throws BadItemException, ZksnarkException { checkAllowShieldedTransactionApi(); @@ -2930,13 +2914,6 @@ public MarketOrderList getMarketOrderListByPair(byte[] sellTokenId, byte[] buyTo return builder.build(); } - public Transaction deployContract(TransactionCapsule trxCap) { - - // do nothing, so can add some useful function later - // trxCap contract para cacheUnpackValue has value - - return trxCap.getInstance(); - } public Transaction triggerContract(TriggerSmartContract triggerSmartContract, @@ -3630,77 +3607,80 @@ public ShieldedTRC20Parameters createShieldedContractParameters( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), dbManager.getDynamicPropertiesStore().disableJavaLangMath())); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("Unbalanced burn!", e); } } - 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", e); } - - return builder.build(true); } private void buildShieldedTRC20InputWithAK( @@ -3763,65 +3743,69 @@ public ShieldedTRC20Parameters createShieldedContractParametersWithoutAsk( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), chainBaseManager.getDynamicPropertiesStore().disableJavaLangMath()); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("Unbalanced burn!", e); } } - 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", e); } - return builder.build(false); } private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddress, @@ -4568,4 +4552,3 @@ public PricesResponseMessage getMemoFeePrices() { return null; } } - diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 63e7ba03fc7..bc50b79a36f 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -418,7 +418,7 @@ public void getAssetIssueByName(BytesMessage request, responseObserver.onNext(wallet.getAssetIssueByName(assetName)); } catch (NonUniqueObjectException e) { responseObserver.onNext(null); - logger.error("Solidity NonUniqueObjectException: {}", e.getMessage()); + logger.debug("Solidity NonUniqueObjectException: {}", e.getMessage()); } } else { responseObserver.onNext(null); diff --git a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java index e574affff6b..ea066a6e98c 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java @@ -19,7 +19,10 @@ public class GetBurnTrxServlet extends RateLimiterServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { try { long value = manager.getDynamicPropertiesStore().getBurnTrxAmount(); - response.getWriter().println("{\"burnTrxAmount\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"burnTrxAmount\": \"" + value + "\"}" + : "{\"burnTrxAmount\": " + value + "}"; + response.getWriter().println(out); } catch (Exception e) { logger.error("", e); try { diff --git a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java index 7e1a5f71841..9788c926586 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java @@ -19,7 +19,10 @@ public class GetPendingSizeServlet extends RateLimiterServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { try { long value = manager.getPendingSize(); - response.getWriter().println("{\"pendingSize\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"pendingSize\": \"" + value + "\"}" + : "{\"pendingSize\": " + value + "}"; + response.getWriter().println(out); } catch (Exception e) { logger.error("", e); try { diff --git a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java index c4d97f46c57..61b88d1160f 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java @@ -24,7 +24,10 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { if (address != null) { value = manager.getMortgageService().queryReward(address); } - response.getWriter().println("{\"reward\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"reward\": \"" + value + "\"}" + : "{\"reward\": " + value + "}"; + response.getWriter().println(out); } catch (DecoderException | IllegalArgumentException e) { try { response.getWriter() diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java index e096df507d7..81c1ece73fb 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java @@ -40,6 +40,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) private void fillResponse(long num, HttpServletResponse response) throws IOException { long count = wallet.getTransactionCountByBlockNum(num); - response.getWriter().println("{\"count\": " + count + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"count\": \"" + count + "\"}" + : "{\"count\": " + count + "}"; + response.getWriter().println(out); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 96dedb1e20c..8a8c66fb371 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -90,6 +90,41 @@ public class JsonFormat { BalanceContract.TransactionBalanceTrace.class ); + /** + * Thread-local flag controlling whether int64/uint64 fields are serialized as JSON strings. + * Set via {@link #setInt64AsString(boolean)} early in request handling and cleared via + * {@link #clearInt64AsString()} in a finally block. Centralized in + * {@code RateLimiterServlet.service} for GET requests. Does not support nested scopes. + */ + private static final ThreadLocal INT64_AS_STRING = + ThreadLocal.withInitial(() -> false); + + /** + * Set whether int64/uint64 protobuf fields are serialized as quoted JSON strings to avoid + * precision loss in clients whose native number type cannot safely represent integers above + * 2^53 - 1 (e.g. JavaScript). Must be paired with {@link #clearInt64AsString()} in a + * finally block. + */ + public static void setInt64AsString(boolean enabled) { + INT64_AS_STRING.set(enabled); + } + + /** + * Clear the int64-as-string thread-local. Always call from a finally block to avoid + * polluting subsequent requests on the same (reused) thread. + */ + public static void clearInt64AsString() { + INT64_AS_STRING.remove(); + } + + /** + * Whether the current thread is in int64-as-string mode. Used by servlets that build + * JSON literals manually (i.e. do not go through {@link #printToString}). + */ + public static boolean isInt64AsString() { + return INT64_AS_STRING.get(); + } + /** * Outputs a textual representation of the Protocol Message supplied into the parameter output. * (This representation is the new version of the classic "ProtocolPrinter" output from the @@ -340,11 +375,8 @@ private static void printFieldValue(FieldDescriptor field, Object value, throws IOException { switch (field.getType()) { case INT32: - case INT64: case SINT32: - case SINT64: case SFIXED32: - case SFIXED64: case FLOAT: case DOUBLE: case BOOL: @@ -352,6 +384,18 @@ private static void printFieldValue(FieldDescriptor field, Object value, generator.print(value.toString()); break; + case INT64: + case SINT64: + case SFIXED64: + if (INT64_AS_STRING.get()) { + generator.print("\""); + generator.print(value.toString()); + generator.print("\""); + } else { + generator.print(value.toString()); + } + break; + case UINT32: case FIXED32: generator.print(unsignedToString((Integer) value)); @@ -359,7 +403,13 @@ private static void printFieldValue(FieldDescriptor field, Object value, case UINT64: case FIXED64: - generator.print(unsignedToString((Long) value)); + if (INT64_AS_STRING.get()) { + generator.print("\""); + generator.print(unsignedToString((Long) value)); + generator.print("\""); + } else { + generator.print(unsignedToString((Long) value)); + } break; case STRING: diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 7a66aed34f6..f173fbcaa82 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -3,7 +3,11 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -31,56 +35,66 @@ @Slf4j public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + + static final Map> ALLOWED_ADAPTERS; + static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + + static { + List> adapters = Arrays.asList( + GlobalPreemptibleAdapter.class, + QpsRateLimiterAdapter.class, + IPQPSRateLimiterAdapter.class, + DefaultBaseQqsAdapter.class); + Map> m = new HashMap<>(); + for (Class c : adapters) { + m.put(c.getSimpleName(), c); + } + ALLOWED_ADAPTERS = Collections.unmodifiableMap(m); + } @Autowired private RateLimiterContainer container; @PostConstruct private void addRateContainer() { - RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() - .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); - boolean success = false; final String name = getClass().getSimpleName(); - if (item != null) { - String cName = ""; - String params = ""; - Object obj; - try { - cName = item.getStrategy(); - params = item.getParams(); - // add the specific rate limiter strategy of servlet. - Class c = Class.forName(ADAPTER_PREFIX + cName); - Constructor constructor; - if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class - || c == IPQPSRateLimiterAdapter.class) { - constructor = c.getConstructor(String.class); - obj = constructor.newInstance(params); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } else { - constructor = c.getConstructor(); - obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } - success = true; - } catch (Exception e) { - this.throwTronError(cName, params, name, e); - } + RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() + .getRateLimiterInitialization().getHttpMap().get(name); + + String cName; + String params; + if (item == null) { + cName = DEFAULT_ADAPTER_NAME; + params = QpsStrategy.DEFAULT_QPS_PARAM; + } else { + cName = item.getStrategy(); + params = item.getParams(); } - if (!success) { - // if the specific rate limiter strategy of servlet is not defined or fail to add, - // then add a default Strategy. - try { - IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); - } catch (Exception e) { - this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e); - } + + try { + container.add(KEY_PREFIX_HTTP, name, buildAdapter(cName, params, name)); + } catch (Exception e) { + throw rateLimiterInitError(cName, params, name, e); + } + } + + static IRateLimiter buildAdapter(String cName, String params, String name) { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c == null) { + throw rateLimiterInitError(cName, params, name, + new IllegalArgumentException("unknown rate limiter adapter; allowed=" + + ALLOWED_ADAPTERS.keySet())); + } + try { + return c.getConstructor(String.class).newInstance(params); + } catch (Exception e) { + throw rateLimiterInitError(cName, params, name, e); } } - private void throwTronError(String strategy, String params, String servlet, Exception e) { - throw new TronError("failure to add the rate limiter strategy. servlet = " + servlet + private static TronError rateLimiterInitError(String strategy, String params, String servlet, + Exception e) { + return new TronError("failure to add the rate limiter strategy. servlet = " + servlet + ", strategy name = " + strategy + ", params = \"" + params + "\".", e, TronError.ErrCode.RATE_LIMITER_INIT); } @@ -102,6 +116,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) String contextPath = req.getContextPath(); String url = Strings.isNullOrEmpty(req.getServletPath()) ? MetricLabels.UNDEFINED : contextPath + req.getServletPath(); + // int64_as_string is honored only on GET requests (URL query). POST is intentionally + // unsupported because reading the body here would consume request.getReader() and + // break downstream servlets that read it themselves. + if ("GET".equalsIgnoreCase(req.getMethod())) { + JsonFormat.setInt64AsString(Util.getInt64AsString(req)); + } try { resp.setContentType("application/json; charset=utf-8"); @@ -119,6 +139,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) } catch (Exception unexpected) { logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected); } finally { + // CRITICAL: this clear pairs with the setInt64AsString call above. Removing it + // will leak int64_as_string state across requests on reused Tomcat threads, + // producing intermittent quoted/unquoted output that is very hard to debug. + JsonFormat.clearInt64AsString(); if (rateLimiter instanceof IPreemptibleRateLimiter && acquireResource) { ((IPreemptibleRateLimiter) rateLimiter).release(); } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..2e3331b2359 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -66,6 +66,7 @@ public class Util { public static final String PERMISSION_ID = "Permission_id"; public static final String VISIBLE = "visible"; + public static final String INT64_AS_STRING_PARAM = "int64_as_string"; public static final String TRANSACTION = "transaction"; public static final String TRANSACTION_EXTENSION = "transactionExtension"; public static final String VALUE = "value"; @@ -95,7 +96,7 @@ public static String printErrorMsg(Exception e) { public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = new JSONObject(); JSONArray jsonArray = new JSONArray(); blocks.stream().forEach(block -> jsonArray.add(printBlockToJSON(block, selfType))); jsonObject.put("block", jsonArray); @@ -110,8 +111,10 @@ public static String printBlock(Block block, boolean selfType) { public static JSONObject printBlockToJSON(Block block, boolean selfType) { BlockCapsule blockCapsule = new BlockCapsule(block); String blockID = ByteArray.toHexString(blockCapsule.getBlockId().getBytes()); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(block, selfType)); + JSONObject jsonObject = new JSONObject(); jsonObject.put("blockID", blockID); + jsonObject.put("block_header", + JSONObject.parseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); if (!blockCapsule.getTransactions().isEmpty()) { jsonObject.put("transactions", printTransactionListToJSON(blockCapsule.getTransactions(), selfType)); @@ -346,6 +349,21 @@ public static boolean existVisible(final HttpServletRequest request) { return Objects.nonNull(request.getParameter(VISIBLE)); } + /** + * Read int64_as_string from URL query parameter. Mirrors + * {@link #getVisible(HttpServletRequest)}. The flag is honored only on GET requests + * (read by {@link RateLimiterServlet#service}); POST requests do not support it + * because that would require caching the request body to allow re-reading by + * downstream servlets. + */ + public static boolean getInt64AsString(final HttpServletRequest request) { + boolean int64AsString = false; + if (StringUtil.isNotBlank(request.getParameter(INT64_AS_STRING_PARAM))) { + int64AsString = Boolean.valueOf(request.getParameter(INT64_AS_STRING_PARAM)); + } + return int64AsString; + } + public static boolean getVisiblePost(final String input) { boolean visible = false; if (StringUtil.isNotBlank(input)) { diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 70673d2148a..104b72a66e8 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -4,6 +4,7 @@ import com.google.common.primitives.Longs; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; @@ -597,6 +598,41 @@ public static long parseBlockTag(String tag, Wallet wallet) throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); } + /** + * Max allowed length for a JSON-RPC block number hex/decimal input. + * API-level DoS guard: rejects pathological inputs before BigInteger parsing, + * whose cost grows quadratically with length. Covers hex (0x + 64 chars for + * uint256) and decimal (78 chars for uint256) representations with headroom. + */ + private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + + /** + * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, + * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit, rejecting + * negative values, and rejecting values that overflow a signed 64-bit + * block number. + */ + public static long parseBlockNumber(String blockNum) + throws JsonRpcInvalidParamsException { + if (blockNum == null || blockNum.length() > MAX_BLOCK_NUM_HEX_LEN) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + BigInteger value; + try { + value = ByteArray.hexToBigInteger(blockNum); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + if (value.signum() < 0) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + try { + return value.longValueExact(); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + } + /** * Parse a block tag or hex number. Uses strict jsonHexToLong (requires 0x prefix) for hex. * Callers needing flexible hex parsing (0x -> hex, bare number -> decimal) should use diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index ea8f15cd088..663b39de290 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -11,6 +11,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.alibaba.fastjson.JSON; @@ -166,6 +167,8 @@ public enum RequestSource { private static final String NO_BLOCK_HEADER_BY_HASH = "header for hash not found"; private static final String ERROR_SELECTOR = "08c379a0"; // Function selector for Error(string) + private static final int REVERT_REASON_SELECTOR_LENGTH = 4; + private static final int MAX_REVERT_REASON_PAYLOAD_BYTES = 4096; /** * thread pool of query section bloom store */ @@ -354,11 +357,7 @@ private void requireLatestBlockTag(String blockNumOrTag) if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); } - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } @@ -368,7 +367,6 @@ private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsEx } private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { - long blockNum; if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { // Return the head block directly from blockStore, bypassing blockIndexStore @@ -377,12 +375,7 @@ private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidPara } return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet)); } - try { - blockNum = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - return wallet.getBlockByNum(blockNum); + return wallet.getBlockByNum(parseBlockNumber(blockNumOrTag)); } private BlockResult getBlockResult(Block block, boolean fullTx) { @@ -492,6 +485,36 @@ private void estimateEnergy(byte[] ownerAddressByte, byte[] contractAddressByte, estimateBuilder.setResult(retBuilder); } + /** + * Decodes an Error(string) revert reason when possible. + * Returns ": reason" for a non-empty reason, otherwise "". + */ + static String tryDecodeRevertReason(byte[] resData) { + if (resData == null || resData.length <= REVERT_REASON_SELECTOR_LENGTH) { + return ""; + } + if (!Hex.toHexString(resData, 0, REVERT_REASON_SELECTOR_LENGTH).equals(ERROR_SELECTOR)) { + return ""; + } + + int revertPayloadLength = resData.length - REVERT_REASON_SELECTOR_LENGTH; + if (revertPayloadLength > MAX_REVERT_REASON_PAYLOAD_BYTES) { + logger.debug("skip parsing oversized revert reason payload: {} bytes", revertPayloadLength); + return ""; + } + + try { + String reason = ContractEventParser.parseDataBytes( + Arrays.copyOfRange(resData, REVERT_REASON_SELECTOR_LENGTH, + resData.length), + "string", 0); + return reason.isEmpty() ? "" : ": " + reason; + } catch (RuntimeException e) { + logger.debug("parse revert reason failed", e); + return ""; + } + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 @@ -535,14 +558,8 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va } result = ByteArray.toJsonHex(listBytes); } else { - String errMsg = retBuilder.getMessage().toStringUtf8(); byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); - if (resData.length > 4 && Hex.toHexString(resData).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(resData, 4, resData.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); if (resData.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData)); @@ -675,15 +692,8 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept } if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) { - String errMsg = retBuilder.getMessage().toStringUtf8(); - byte[] data = trxExtBuilder.getConstantResult(0).toByteArray(); - if (data.length > 4 && Hex.toHexString(data).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(data, 4, data.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data); if (data.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data)); @@ -969,12 +979,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + long blockNumber = parseBlockNumber(blockNumOrTag); if (wallet.getBlockByNum(blockNumber) == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java index 57650355d46..4f11c1a5908 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java @@ -98,7 +98,7 @@ public TransactionResult(BlockCapsule blockCapsule, int index, Protocol.Transact TransactionCapsule capsule = new TransactionCapsule(tx); byte[] txId = capsule.getTransactionId().getBytes(); hash = ByteArray.toJsonHex(txId); - nonce = ByteArray.toJsonHex(new byte[8]); // no value + nonce = "0x0"; // no value, QUANTITY type per Ethereum JSON-RPC spec blockHash = ByteArray.toJsonHex(blockCapsule.getBlockId().getBytes()); blockNumber = ByteArray.toJsonHex(blockCapsule.getNum()); transactionIndex = ByteArray.toJsonHex(index); @@ -133,7 +133,7 @@ public TransactionResult(Transaction tx, Wallet wallet) { TransactionCapsule capsule = new TransactionCapsule(tx); byte[] txId = capsule.getTransactionId().getBytes(); hash = ByteArray.toJsonHex(txId); - nonce = ByteArray.toJsonHex(new byte[8]); // no value + nonce = "0x0"; // no value, QUANTITY type per Ethereum JSON-RPC spec blockHash = "0x"; blockNumber = "0x"; transactionIndex = "0x"; 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/common/logsfilter/EventParserTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java index 0d2a8ed1496..8e6b366fef8 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java @@ -5,6 +5,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.bouncycastle.crypto.OutputLengthException; import org.bouncycastle.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -100,6 +101,91 @@ public synchronized void testEventParser() { } + @Test + public void testParseDataBytesIntegerTypes() { + // uint256 = 255 + byte[] uintData = ByteArray.fromHexString( + "00000000000000000000000000000000000000000000000000000000000000ff"); + Assert.assertEquals("255", ContractEventParser.parseDataBytes(uintData, "uint256", 0)); + + // int256 = -1 (two's complement 0xFF..FF is signed negative one) + byte[] negIntData = ByteArray.fromHexString( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + Assert.assertEquals("-1", ContractEventParser.parseDataBytes(negIntData, "int256", 0)); + + // trcToken is classified as INT_NUMBER + byte[] tokenData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000064"); + Assert.assertEquals("100", ContractEventParser.parseDataBytes(tokenData, "trcToken", 0)); + } + + @Test + public void testParseDataBytesBool() { + byte[] trueData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("true", ContractEventParser.parseDataBytes(trueData, "bool", 0)); + + byte[] falseData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("false", ContractEventParser.parseDataBytes(falseData, "bool", 0)); + } + + @Test + public void testParseDataBytesFixedBytes() { + String hex = "1234567890abcdef0000000000000000000000000000000000000000000000ff"; + byte[] data = ByteArray.fromHexString(hex); + Assert.assertEquals(hex, ContractEventParser.parseDataBytes(data, "bytes32", 0)); + } + + @Test + public void testParseDataBytesAddress() { + Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); + // last 20 bytes = ca35...733c => Base58Check = TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt + byte[] data = ByteArray.fromHexString( + "000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c"); + Assert.assertEquals("TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt", + ContractEventParser.parseDataBytes(data, "address", 0)); + } + + @Test + public void testParseDataBytesDynamicBytes() { + // offset 0x20 | length 3 | 0x010203 padded to 32 bytes + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "0102030000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("010203", ContractEventParser.parseDataBytes(data, "bytes", 0)); + } + + @Test + public void testParseDataBytesEmptyString() { + // offset 0x20 | length 0 + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesNonEmptyString() { + // "hello world" is 11 ASCII bytes (68656c6c6f20776f726c64), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000b" + + "68656c6c6f20776f726c64000000000000000000000000000000000000000000"); + Assert.assertEquals("hello world", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesMultiByteUtf8String() { + // "中文" UTF-8 = e4b8ad e69687 (6 bytes), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000006" + + "e4b8ade696870000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("中文", ContractEventParser.parseDataBytes(data, "string", 0)); + } + @Test public void testParseRevert() { String dataHex = "08c379a0" @@ -113,4 +199,87 @@ public void testParseRevert() { Assert.assertEquals(msg, "not enough input value"); } + + @Test + public void testSubBytesRejectsOversizedLength() { + // Length must fit in the available source bytes. Reject instead of + // truncating so oversized ABI lengths are not silently coerced. + byte[] src = new byte[]{1, 2, 3}; + try { + ContractEventParser.subBytes(src, 0, Integer.MAX_VALUE); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:0")); + Assert.assertTrue(e.getMessage().contains("length:2147483647")); + Assert.assertTrue(e.getMessage().contains("src.length:3")); + } + } + + @Test + public void testSubBytesAcceptsExactLength() { + byte[] src = new byte[]{1, 2, 3, 4}; + byte[] result = ContractEventParser.subBytes(src, 1, 3); + Assert.assertArrayEquals(new byte[]{2, 3, 4}, result); + } + + @Test + public void testSubBytesRejectsNegativeOffset() { + // ABI offsets are unsigned, but BigInteger(byte[]) interprets 0xFF..FF as + // -1. The guard should reject that value before System.arraycopy runs. + byte[] src = new byte[]{1, 2, 3, 4}; + try { + ContractEventParser.subBytes(src, -1, 3); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:-1")); + Assert.assertTrue(e.getMessage().contains("length:3")); + Assert.assertTrue(e.getMessage().contains("src.length:4")); + } + } + + @Test + public void testSubBytesRejectsEmptySource() { + try { + ContractEventParser.subBytes(new byte[0], 0, 0); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("source data is empty")); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeOffset() { + // End-to-end check: an offset field of 0xFF..FF decodes to -1 and should + // be rejected through the existing UnsupportedOperationException path. + String dataHex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsMalformedLength() { + // ABI-encoded "string" whose declared length exceeds the available payload + // should be rejected via the existing UnsupportedOperationException path. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeLength() { + // ABI length is an unsigned word. If 0xFF..FF is decoded as -1, reject it + // instead of treating it as an empty string/bytes payload. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } } diff --git a/framework/src/test/java/org/tron/core/ShieldWalletTest.java b/framework/src/test/java/org/tron/core/ShieldWalletTest.java index b9fa48dca38..0353d260eff 100644 --- a/framework/src/test/java/org/tron/core/ShieldWalletTest.java +++ b/framework/src/test/java/org/tron/core/ShieldWalletTest.java @@ -4,15 +4,18 @@ import static org.mockito.Mockito.spy; import static org.tron.core.zen.ZksnarkInitService.librustzcashInitZksnarkParams; +import com.google.protobuf.ByteString; import java.math.BigInteger; import javax.annotation.Resource; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.PrivateParameters; import org.tron.api.GrpcAPI.PrivateParametersWithoutAsk; import org.tron.api.GrpcAPI.PrivateShieldedTRC20Parameters; import org.tron.api.GrpcAPI.PrivateShieldedTRC20ParametersWithoutAsk; +import org.tron.api.GrpcAPI.ReceiveNote; import org.tron.api.GrpcAPI.ShieldedAddressInfo; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; import org.tron.common.BaseTest; @@ -22,6 +25,7 @@ import org.tron.core.config.args.Args; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ZksnarkException; import org.tron.core.services.http.JsonFormat; import org.tron.core.services.http.JsonFormat.ParseException; @@ -450,4 +454,226 @@ public void testCreateShieldedContractParametersWithoutAsk() throws ContractExeE Assert.fail(); } } + + private static final byte[] SHIELDED_CONTRACT_ADDRESS = + ByteArray.fromHexString("4144007979359ECAC395BBD3CEF8060D3DF2DC3F01"); + private static final String VALID_PAYMENT_ADDR = + "ztron1y99u6ejqenupvfkp5g6q6yqkp0a44c48cta0dd5gejtqa4v27hqa2cghfvdxnmneh6qqq03fa75"; + + private Wallet newSpyWallet() throws ContractExeException { + Args.getInstance().setAllowShieldedTransactionApi(true); + Wallet wallet1 = spy(new Wallet()); + doReturn(BigInteger.valueOf(1).toByteArray()) + .when(wallet1).getShieldedContractScalingFactor(SHIELDED_CONTRACT_ADDRESS); + return wallet1; + } + + private GrpcAPI.SpendNoteTRC20 spendNoteOfValue(long value) { + GrpcAPI.Note note = GrpcAPI.Note.newBuilder() + .setValue(value) + .setPaymentAddress(VALID_PAYMENT_ADDR) + .setRcm(ByteString.copyFrom(new byte[32])) + .setMemo(ByteString.copyFrom(new byte[512])) + .build(); + return GrpcAPI.SpendNoteTRC20.newBuilder() + .setNote(note) + .setAlpha(ByteString.copyFrom(new byte[32])) + .setRoot(ByteString.copyFrom(new byte[32])) + .setPath(ByteString.copyFrom(new byte[1024])) + .setPos(0) + .build(); + } + + private ReceiveNote receiveNoteOfValue(long value) { + GrpcAPI.Note note = GrpcAPI.Note.newBuilder() + .setValue(value) + .setPaymentAddress(VALID_PAYMENT_ADDR) + .setRcm(ByteString.copyFrom(new byte[32])) + .setMemo(ByteString.copyFrom(new byte[512])) + .build(); + return ReceiveNote.newBuilder().setNote(note).build(); + } + + @Test + public void testCreateShieldedContractParameters_invalidParams() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("invalid shielded TRC-20 parameters", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_TRANSFER_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ask, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_BURN_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ask, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_BURN_missingTransparentTo() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .setAsk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No valid transparent TRC-20 output address", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_TRANSFER_arithmeticOverflow() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedReceives(receiveNoteOfValue(0)) + .setAsk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ZksnarkException); + Assert.assertEquals("shielded amount overflow", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_invalidParams() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("invalid shielded TRC-20 parameters", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_TRANSFER_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ak, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_BURN_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ak, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_BURN_missingTransparentTo() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .setAk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No transparent TRC-20 output address", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_MINT_emptyOvk() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .setFromAmount("100") + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + try { + ShieldedTRC20Parameters params = wallet1.createShieldedContractParametersWithoutAsk(request); + Assert.assertNotNull(params); + } catch (Exception e) { + Assert.fail("MINT with empty ovk should auto-generate one: " + e); + } + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_TRANSFER_arithmeticOverflow() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedReceives(receiveNoteOfValue(0)) + .setAk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ZksnarkException); + Assert.assertEquals("shielded amount overflow", e.getMessage()); + } } 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 e0d9d456e9a..45bc7c049e3 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 @@ -415,5 +415,14 @@ public void testFetchBlockTimeoutInRangeUnchanged() { Assert.assertEquals(500, Args.getInstance().getFetchBlockTimeout()); 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/jsonrpc/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java new file mode 100644 index 00000000000..65defdab2ed --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -0,0 +1,277 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.GrpcAPI.EstimateEnergyMessage; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.GrpcAPI.TransactionExtention; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.Manager; +import org.tron.core.exception.jsonrpc.JsonRpcInternalException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class JsonRpcCallAndEstimateGasTest { + + private static final String ERROR_REVERT_HEX = "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"; + private static final String REVERT_MSG = "REVERT opcode executed"; + private static final String MOCK_FROM_ADDRESS = "0x0000000000000000000000000000000000000000"; + private static final String MOCK_TO_ADDRESS = "0x0000000000000000000000000000000000000001"; + + private enum EstimatePath { + CONSTANT_CALL, + ESTIMATE_ENERGY + } + + private final boolean originalEstimateEnergy = CommonParameter.getInstance().isEstimateEnergy(); + private TronJsonRpcImpl mockRpc; + + @After + public void tearDown() throws Exception { + if (mockRpc != null) { + mockRpc.close(); + mockRpc = null; + } + CommonParameter.getInstance().setEstimateEnergy(originalEstimateEnergy); + } + + @Test + public void testGetCallAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testGetCallSkipsRevertReasonForPanicSelector() throws Exception { + byte[] panicData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + + mockRpc = newRpcWithMockedFailedCall(panicData, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testGetCallSkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testEstimateGasSkipsRevertReasonForEmptyData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergyAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergySkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exception { + long energyRequired = 0x4321L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyRequired, + EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result); + } + + @Test + public void testGetCallReturnsConstantResult() throws Exception { + byte[] part1 = ByteArray.fromHexString("deadbeef"); + byte[] part2 = ByteArray.fromHexString("cafebabe"); + + mockRpc = newRpcWithMockedSuccessfulCall(part1, part2); + + String result = mockRpc.getCall(newCallArgs(), "latest"); + + Assert.assertEquals("0xdeadbeefcafebabe", result); + } + + @Test + public void testEstimateGasReturnsEnergyUsed() throws Exception { + long energyUsed = 0x1234L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyUsed, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyUsed), result); + } + + private static CallArguments newCallArgs() { + CallArguments args = new CallArguments(); + args.setFrom(MOCK_FROM_ADDRESS); + args.setTo(MOCK_TO_ADDRESS); + args.setValue("0x0"); + args.setData("0x"); + return args; + } + + private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, EstimatePath path) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + estimateBuilder.setResult(retBuilder); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + for (byte[] bytes : constantResults) { + extBuilder.addConstantResult(ByteString.copyFrom(bytes)); + } + extBuilder.setEnergyUsed(0L); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedEstimateGasSuccessfulCall(long energyValue, + EstimatePath path) throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + estimateBuilder.setEnergyRequired(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + extBuilder.setEnergyUsed(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } +} 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 94b05cae98f..e87b2566205 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -149,6 +149,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/services/http/JsonFormatInt64AsStringTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java new file mode 100644 index 00000000000..77ea73999d1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java @@ -0,0 +1,264 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import com.google.protobuf.UInt64Value; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Test; +import org.tron.protos.Protocol; + +/** + * Tests for {@link JsonFormat#setInt64AsString(boolean)} / + * {@link JsonFormat#clearInt64AsString()} / {@link JsonFormat#isInt64AsString()}. + * + *

Tron protos do not define uint64/fixed64 fields directly; all 64-bit values use int64. + * The uint64 branch is exercised using {@link com.google.protobuf.UInt64Value}, a protobuf + * well-known wrapper with a single {@code uint64 value} field. + */ +public class JsonFormatInt64AsStringTest { + + /** Defensive cleanup in case a test leaves the ThreadLocal dirty. */ + @After + public void clearState() { + JsonFormat.clearInt64AsString(); + } + + @Test + public void defaultBehaviorUnchangedWhenUnset() { + Protocol.Account account = Protocol.Account.newBuilder() + .setBalance(123456789012345L) + .build(); + String out = JsonFormat.printToString(account, true); + assertTrue("expected unquoted balance, got: " + out, + out.contains("\"balance\":123456789012345") + || out.contains("\"balance\": 123456789012345")); + assertFalse("balance should not be quoted by default, got: " + out, + out.contains("\"balance\":\"123456789012345\"") + || out.contains("\"balance\": \"123456789012345\"")); + } + + @Test + public void int64FieldQuotedWhenSet() { + Protocol.Account account = Protocol.Account.newBuilder() + .setBalance(123456789012345L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("expected quoted balance, got: " + out, + out.contains("\"123456789012345\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void uint64FieldQuotedWhenSet() { + UInt64Value v = UInt64Value.of(9007199254740993L); // 2^53 + 1 + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(v, true); + assertTrue("expected quoted uint64 value, got: " + out, + out.contains("\"9007199254740993\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void uint64DefaultUnquoted() { + UInt64Value v = UInt64Value.of(9007199254740993L); + String out = JsonFormat.printToString(v, true); + assertTrue("expected unquoted uint64 value, got: " + out, + out.contains("9007199254740993")); + assertFalse("uint64 should not be quoted by default, got: " + out, + out.contains("\"9007199254740993\"")); + } + + @Test + public void stringBytesEnumNotAffected() { + // Note: proto3 does not serialize default-valued fields, so enum/bytes fields are + // set to non-default values to verify they appear in the output. + Protocol.Account account = Protocol.Account.newBuilder() + .setAccountName(ByteString.copyFromUtf8("alice")) + .setType(Protocol.AccountType.AssetIssue) // non-default enum value + .setBalance(1L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + // balance int64 should be quoted + assertTrue("balance should be quoted, got: " + out, out.contains("\"1\"")); + // enum type serialized by name (not a number), not affected by int64_as_string + assertTrue("enum type should appear as name, got: " + out, + out.contains("AssetIssue")); + // bytes account_name should still serialize normally + assertTrue("account_name should appear, got: " + out, out.contains("account_name")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void nestedInt64FieldsQuoted() { + Protocol.Block block = Protocol.Block.newBuilder() + .setBlockHeader(Protocol.BlockHeader.newBuilder() + .setRawData(Protocol.BlockHeader.raw.newBuilder() + .setNumber(9007199254740993L) // 2^53 + 1 + .setTimestamp(1700000000000L) + .build()) + .build()) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(block, true); + assertTrue("nested number should be quoted, got: " + out, + out.contains("\"9007199254740993\"")); + assertTrue("nested timestamp should be quoted, got: " + out, + out.contains("\"1700000000000\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void mapStringInt64ValuesQuoted() { + Protocol.Account account = Protocol.Account.newBuilder() + .putAsset("USDT", 123456789012345L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("map value should be quoted, got: " + out, + out.contains("\"123456789012345\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void boundaryValuesAllQuoted() { + // Note: proto3 does not serialize a field whose value equals its type default (0 for int64), + // so 0L is covered separately via defaultBehaviorUnchangedWhenUnset / uint64DefaultUnquoted + // (both use non-default values) and does not need an explicit quoted-output test. + long[] values = { + (1L << 53) - 1, // max safe JS integer + 1L << 53, // boundary + (1L << 53) + 1, // first unsafe + Long.MAX_VALUE, + Long.MIN_VALUE, + -1L + }; + for (long v : values) { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(v).build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("value=" + v + " expected quoted, got: " + out, + out.contains("\"" + v + "\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + } + + @Test + public void clearResetsState() { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + JsonFormat.setInt64AsString(true); + JsonFormat.clearInt64AsString(); + String out = JsonFormat.printToString(account, true); + assertFalse("state should be cleared, got: " + out, out.contains("\"1\"")); + } + + @Test + public void clearInFinallySurvivesException() { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + JsonFormat.setInt64AsString(true); + try { + throw new RuntimeException("boom"); + } catch (RuntimeException expected) { + // expected + } finally { + JsonFormat.clearInt64AsString(); + } + String out = JsonFormat.printToString(account, true); + assertFalse("state leaked after exception, got: " + out, out.contains("\"1\"")); + } + + @Test + public void isInt64AsStringReflectsCurrentState() { + assertFalse(JsonFormat.isInt64AsString()); + JsonFormat.setInt64AsString(true); + try { + assertTrue(JsonFormat.isInt64AsString()); + } finally { + JsonFormat.clearInt64AsString(); + } + assertFalse(JsonFormat.isInt64AsString()); + } + + @Test + public void threadIsolation() throws Exception { + final Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + final CountDownLatch barrier = new CountDownLatch(2); + ExecutorService ex = Executors.newFixedThreadPool(2); + try { + Future trueThread = ex.submit(() -> { + JsonFormat.setInt64AsString(true); + try { + barrier.countDown(); + barrier.await(); + return JsonFormat.printToString(account, true); + } finally { + JsonFormat.clearInt64AsString(); + } + }); + Future falseThread = ex.submit(() -> { + barrier.countDown(); + barrier.await(); + return JsonFormat.printToString(account, true); + }); + String withSet = trueThread.get(5, TimeUnit.SECONDS); + String noSet = falseThread.get(5, TimeUnit.SECONDS); + assertTrue("trueThread should see quoted: " + withSet, + withSet.contains("\"1\"")); + assertFalse("falseThread should see unquoted: " + noSet, + noSet.contains("\"1\"")); + } finally { + ex.shutdownNow(); + } + } + + @Test + public void noPollutionOnThreadReuse() throws Exception { + final Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + ExecutorService single = Executors.newSingleThreadExecutor(); + try { + Future firstRun = single.submit(() -> { + JsonFormat.setInt64AsString(true); + try { + return JsonFormat.printToString(account, true); + } finally { + JsonFormat.clearInt64AsString(); + } + }); + assertTrue(firstRun.get(5, TimeUnit.SECONDS).contains("\"1\"")); + + // Reuse the same thread; without a new set, state must be cleared. + Future secondRun = single.submit(() -> JsonFormat.printToString(account, true)); + String second = secondRun.get(5, TimeUnit.SECONDS); + assertFalse("thread reuse leaked quoted state: " + second, + second.contains("\"1\"")); + } finally { + single.shutdownNow(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java new file mode 100644 index 00000000000..882c5f99833 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java @@ -0,0 +1,164 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.UnsupportedEncodingException; +import javax.annotation.Resource; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; + +/** + * End-to-end integration tests for {@link RateLimiterServlet#service} wiring of the + * {@code int64_as_string} flag. The single-class {@link JsonFormatInt64AsStringTest} verifies + * the {@code JsonFormat} ThreadLocal mechanism in isolation; this test verifies the full + * request-handling chain: URL query --> {@code service()} --> ThreadLocal --> output, and + * the {@code finally} clear that prevents state leakage across reused threads. + * + *

Pins four contracts: + *

    + *
  1. GET with {@code ?int64_as_string=true} produces quoted int64 fields.
  2. + *
  3. GET without the flag produces unquoted int64 fields (regression baseline).
  4. + *
  5. POST never honors the flag, regardless of source -- GET-only is the documented + * contract under issue #6568.
  6. + *
  7. {@code service()}'s {@code finally} block clears the ThreadLocal so reused Tomcat + * threads do not leak state between requests.
  8. + *
+ * + *

Uses {@link GetNowBlockServlet} as the fixture servlet because its response goes through + * {@code JsonFormat.printToString}, which is what the ThreadLocal actually controls. + */ +public class RateLimiterServletInt64Test extends BaseTest { + + @Resource(name = "getNowBlockServlet") + private GetNowBlockServlet servlet; + + @Resource(name = "getBurnTrxServlet") + private GetBurnTrxServlet handBuiltServlet; + + static { + Args.setParam( + new String[]{ + "--output-directory", dbPath(), + }, TestConstants.TEST_CONF + ); + } + + @Before + public void clearBefore() { + JsonFormat.clearInt64AsString(); + } + + @After + public void clearAfter() { + JsonFormat.clearInt64AsString(); + } + + /** Contract 1: GET with int64_as_string=true on URL query produces quoted int64 fields. */ + @Test + public void getWithUrlFlagQuotesInt64() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertTrue("timestamp should be quoted when int64_as_string=true, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\"\\d+\".*")); + } + } + + /** Contract 2: GET without flag produces unquoted int64 fields (default behavior). */ + @Test + public void getWithoutFlagKeepsUnquoted() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertTrue("timestamp should be unquoted when no flag, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\\d+.*")); + } + } + + /** + * Contract 3: POST never honors int64_as_string, regardless of where the flag is placed. + * Pins the GET-only design contract for issue #6568. Any future PR that tries to extend + * support to POST will fail this test, forcing an explicit design review. + */ + @Test + public void postWithUrlFlagIgnored() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertFalse("POST URL flag must be ignored under GET-only design, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\"\\d+\".*")); + } + } + + /** + * Contract 4 (CRITICAL): service() must clear the ThreadLocal in finally. Without this + * clear, reused Tomcat threads leak the flag across requests, producing intermittent + * quoted/unquoted output that is extremely hard to debug in production. + */ + @Test + public void serviceClearsThreadLocalInFinally() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + servlet.service(request, new MockHttpServletResponse()); + assertFalse( + "RateLimiterServlet.service must clear int64_as_string ThreadLocal in its finally " + + "block. Removing this clear will leak state across requests on reused threads.", + JsonFormat.isInt64AsString()); + } + + /** + * Contract 5: hand-built JSON servlets (the ones that emit JSON literals manually instead + * of going through {@link JsonFormat#printToString}) honor the flag. The previous tests use + * {@link GetNowBlockServlet} which goes through {@code printToString}; this test uses + * {@link GetBurnTrxServlet} as a representative of the four ternary-style servlets + * (GetBurnTrx / GetPendingSize / GetTransactionCountByBlockNum / GetReward) to lock down + * their {@code isInt64AsString() ? quoted : unquoted} branch -- so a future refactor that + * inverts the ternary or breaks the quote placement fails visibly here. + */ + @Test + public void handBuiltJsonServletQuotesInt64WhenFlagSet() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + handBuiltServlet.service(request, response); + String body = readBody(response); + assertTrue("burnTrxAmount should be quoted when int64_as_string=true, got: " + body, + body.matches("(?s).*\"burnTrxAmount\"\\s*:\\s*\"\\d+\".*")); + } + + /** Contract 6: hand-built JSON servlets default to unquoted output (regression baseline). */ + @Test + public void handBuiltJsonServletKeepsUnquotedByDefault() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + handBuiltServlet.service(request, response); + String body = readBody(response); + assertTrue("burnTrxAmount should be unquoted by default, got: " + body, + body.matches("(?s).*\"burnTrxAmount\"\\s*:\\s*\\d+.*")); + } + + private String readBody(MockHttpServletResponse response) throws UnsupportedEncodingException { + return response.getContentAsString(); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java new file mode 100644 index 00000000000..4ae76f85dfb --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -0,0 +1,93 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import org.junit.Test; +import org.tron.core.exception.TronError; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Verifies RateLimiterServlet's adapter resolution: strict whitelist + * (no Class.forName arbitrary class loading), fail-fast on unknown or + * empty names, and successful construction of every whitelisted adapter. + */ +public class RateLimiterServletTest { + + private static final Map> allowedAdapters = + RateLimiterServlet.ALLOWED_ADAPTERS; + + @Test + public void testWhitelistContents() { + assertEquals(GlobalPreemptibleAdapter.class, + allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertEquals(QpsRateLimiterAdapter.class, + allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertEquals(IPQPSRateLimiterAdapter.class, + allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertEquals(DefaultBaseQqsAdapter.class, + allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); + } + + @Test + public void testWhitelistRejectsUnknownAdapter() { + assertNull(allowedAdapters.get("EvilAdapter")); + assertNull(allowedAdapters.get("java.lang.Runtime")); + } + + @Test + public void testUnknownAdapterThrowsTronError() { + // Fail-fast parity with the pre-whitelist Class.forName behavior: an unknown + // adapter name raises TronError from @PostConstruct so Spring startup aborts + // rather than silently masking a misconfigured node. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("UnknownAdapter", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + assertTrue(e.getMessage().contains("UnknownAdapter")); + assertTrue(e.getMessage().contains("TestServlet")); + } + + @Test + public void testDefaultAdapterNameBuildsDefaultBaseQqsAdapter() { + // When no config entry exists for a servlet, addRateContainer passes + // DEFAULT_ADAPTER_NAME to buildAdapter; verify it resolves to + // DefaultBaseQqsAdapter. + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); + } + + @Test + public void testEmptyAdapterNameThrowsTronError() { + // Fail-fast parity with original: a configured-but-empty strategy name is + // a configuration bug and must not be silently replaced by the default. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + } + + @Test + public void testBuildsEachWhitelistedAdapter() { + // Exercises the newInstance(String) constructor path for every whitelisted + // adapter so a signature/strategy-class break on any entry fails here + // instead of at node startup. + assertTrue(RateLimiterServlet.buildAdapter( + QpsRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof QpsRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + IPQPSRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof IPQPSRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + GlobalPreemptibleAdapter.class.getSimpleName(), "permit=1", "TestServlet") + instanceof GlobalPreemptibleAdapter); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 221c5a7a165..7c05e0e9cfe 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -1,11 +1,15 @@ package org.tron.core.services.http; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; import java.security.InvalidParameterException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.junit.After; @@ -44,7 +48,7 @@ public void testPrintTransactionFee() { public void testPrintBlockList() { BlockCapsule blockCapsule1 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - BlockCapsule blockCapsule2 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + BlockCapsule blockCapsule2 = new BlockCapsule(2, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); GrpcAPI.BlockList list = GrpcAPI.BlockList.newBuilder() .addBlock(blockCapsule1.getInstance()) @@ -52,6 +56,148 @@ public void testPrintBlockList() { .build(); String out = Util.printBlockList(list, true); Assert.assertNotNull(out); + + JSONObject json = JSONObject.parseObject(out); + Assert.assertTrue(json.containsKey("block")); + JSONArray blockArray = json.getJSONArray("block"); + Assert.assertEquals(2, blockArray.size()); + + // verify each block has correct structure + for (int i = 0; i < blockArray.size(); i++) { + JSONObject blockJson = blockArray.getJSONObject(i); + Assert.assertTrue(blockJson.containsKey("blockID")); + Assert.assertTrue(blockJson.containsKey("block_header")); + Assert.assertFalse(blockJson.getString("blockID").isEmpty()); + JSONObject blockHeader = blockJson.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONEmptyTransactions() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertFalse(json.containsKey("transactions")); + Assert.assertFalse(json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + + @Test + public void testPrintBlockToJSONWithTransactions() { + // Structural invariants must hold under either visible flag; the flag-driven + // encoding difference is covered by testPrintBlockToJSONVisibleFlagAffectsAddressEncoding. + for (boolean visible : new boolean[]{true, false}) { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + blockCapsule.addTransaction(getTransactionCapsuleExample()); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), visible); + + String msg = "visible=" + visible; + Assert.assertTrue(msg, json.containsKey("blockID")); + Assert.assertTrue(msg, json.containsKey("block_header")); + Assert.assertTrue(msg, json.containsKey("transactions")); + Assert.assertFalse(msg, json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(msg, blockHeader); + Assert.assertTrue(msg, blockHeader.containsKey("raw_data")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(msg, 1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(msg, txJson.containsKey("txID")); + Assert.assertTrue(msg, txJson.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONVisibleFlagAffectsAddressEncoding() { + // Pins the optimized printBlockToJSON against the prior behavior: the + // visible flag must still thread through to JsonFormat so address-bearing + // fields switch encoding while byte-identity fields stay stable. + ByteString witnessAddress = ByteString.copyFrom( + ByteArray.fromHexString("41548794500882809695a8a687866e76d4271a1abc")); + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), witnessAddress); + + JSONObject visible = Util.printBlockToJSON(blockCapsule.getInstance(), true); + JSONObject hidden = Util.printBlockToJSON(blockCapsule.getInstance(), false); + + // blockID is derived from raw bytes; identical under either flag. + Assert.assertEquals(visible.getString("blockID"), hidden.getString("blockID")); + + // Overall block_header must differ because witness_address is re-encoded. + String headerVisible = visible.getJSONObject("block_header").toJSONString(); + String headerHidden = hidden.getJSONObject("block_header").toJSONString(); + Assert.assertNotEquals(headerVisible, headerHidden); + + // visible=true renders a mainnet address as Base58 starting with 'T'. + String witnessVisible = visible.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessVisible); + Assert.assertTrue("visible=true witness_address should be Base58 ('T...'), got: " + + witnessVisible, witnessVisible.startsWith("T")); + + // visible=false keeps witness_address in raw (non-Base58) form. + String witnessHidden = hidden.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessHidden); + Assert.assertNotEquals(witnessVisible, witnessHidden); + } + + @Test + public void testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl() { + // Legacy impl produced JSON via JsonFormat.printToString(block, selfType), + // which omits repeated fields when empty. New impl mirrors that with an + // explicit isEmpty() guard. Pin parity using JsonFormat output as ground + // truth so a future refactor can't quietly start emitting "transactions": [] + // (or stop emitting the key when non-empty). + BlockCapsule empty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + assertTransactionsKeyMatchesLegacy(empty.getInstance(), false); + + BlockCapsule nonEmpty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + nonEmpty.addTransaction(getTransactionCapsuleExample()); + assertTransactionsKeyMatchesLegacy(nonEmpty.getInstance(), true); + } + + private static void assertTransactionsKeyMatchesLegacy(Protocol.Block block, + boolean expectTransactionsKey) { + JSONObject legacy = JSONObject.parseObject(JsonFormat.printToString(block, true)); + Assert.assertEquals("legacy JsonFormat parity broken — proto behavior changed?", + expectTransactionsKey, legacy.containsKey("transactions")); + + JSONObject actual = Util.printBlockToJSON(block, true); + Assert.assertEquals("new impl diverged from legacy on 'transactions' key presence", + expectTransactionsKey, actual.containsKey("transactions")); + } + + @Test + public void testPrintBlockToJSONCoversAllProtoTopLevelFields() { + // Guards against proto field drift: the old impl delegated to JsonFormat on + // the whole Block message, so any new top-level Block field appeared + // automatically. The new impl hand-assembles the JSON, so a future proto + // field would be silently dropped. Reflect over Block's descriptor and + // assert every declared top-level field is handled. + Map protoFieldToJsonKey = new HashMap<>(); + protoFieldToJsonKey.put("block_header", "block_header"); + // "transactions" is present only when non-empty; parity verified in + // testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl. + protoFieldToJsonKey.put("transactions", "transactions"); + + for (Descriptors.FieldDescriptor f : Protocol.Block.getDescriptor().getFields()) { + Assert.assertTrue( + "Block proto field '" + f.getName() + "' is not handled by printBlockToJSON. " + + "If you added a new top-level field, extend printBlockToJSON and this test.", + protoFieldToJsonKey.containsKey(f.getName())); + } } @Test diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java new file mode 100644 index 00000000000..6aaeea2cc4e --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -0,0 +1,78 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +public class JsonRpcApiUtilTest { + + @Test + public void parseBlockNumberAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(0x1aL, JsonRpcApiUtil.parseBlockNumber("0x1a")); + assertEquals(0L, JsonRpcApiUtil.parseBlockNumber("0x0")); + } + + @Test + public void parseBlockNumberAcceptsDecimal() throws JsonRpcInvalidParamsException { + assertEquals(12345L, JsonRpcApiUtil.parseBlockNumber("12345")); + } + + @Test + public void parseBlockNumberAcceptsMaxLongValue() throws JsonRpcInvalidParamsException { + assertEquals(Long.MAX_VALUE, + JsonRpcApiUtil.parseBlockNumber("0x7fffffffffffffff")); + } + + @Test + public void parseBlockNumberRejectsNegative() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("-1")); + assertEquals("invalid block number", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x-1")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOverflow() { + // 2^64 - 1: fits uint64 but overflows signed long + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xffffffffffffffff")); + assertEquals("invalid block number", e1.getMessage()); + // 2^63: just past Long.MAX_VALUE + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x8000000000000000")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOversized() { + // 101 chars exceeds the 100-char limit + String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(null)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xGG")); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsEmpty() { + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java index 4e1af06199c..19c2bb6c4d3 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java @@ -23,10 +23,18 @@ public class TransactionResultTest extends BaseTest { private static final String OWNER_ADDRESS = "41548794500882809695a8a687866e76d4271a1abc"; private static final String CONTRACT_ADDRESS = "A0B4750E2CD76E19DCA331BF5D089B71C3C2798548"; + // QUANTITY pattern from ethereum/execution-apis base-types schema (uint). + private static final String QUANTITY_PATTERN = "^0x(0|[1-9a-f][0-9a-f]*)$"; + static { Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); } + private static void assertQuantity(String value) { + Assert.assertNotNull(value); + Assert.assertTrue("not a valid QUANTITY: " + value, value.matches(QUANTITY_PATTERN)); + } + @Test public void testBuildTransactionResultWithBlock() { SmartContractOuterClass.TriggerSmartContract.Builder builder2 = @@ -49,6 +57,8 @@ public void testBuildTransactionResultWithBlock() { transactionResult.getHash()); Assert.assertEquals(transactionResult.getGasPrice(), "0x1"); Assert.assertEquals(transactionResult.getGas(), "0x64"); + Assert.assertEquals("0x0", transactionResult.getNonce()); + assertQuantity(transactionResult.getNonce()); } @Test @@ -65,7 +75,8 @@ public void testBuildTransactionResult() { Assert.assertEquals("0x5691531881bc44adbc722060d85fdf29265823db8e884b0d104fcfbba253cf11", transactionResult.getHash()); Assert.assertEquals(transactionResult.getGasPrice(), "0x"); - Assert.assertEquals(transactionResult.getNonce(), "0x0000000000000000"); + Assert.assertEquals("0x0", transactionResult.getNonce()); + assertQuantity(transactionResult.getNonce()); } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java new file mode 100644 index 00000000000..8d72aeed04f --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java @@ -0,0 +1,97 @@ +package org.tron.core.services.jsonrpc; + +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.utils.ByteArray; + +public class TronJsonRpcRevertReasonTest { + + @Test + public void testTryDecodeRevertReasonWithMalformedLength() { + // Error(string) selector + offset=0x20 + length=0x7FFFFFFF + 3 bytes of payload. + // parseDataBytes throws because the declared length exceeds the buffer. + // The helper should return "" and leave the raw revert hex untouched. + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNegativeLength() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithValidData() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"); + Assert.assertEquals(": not enough input value", + TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithEmptyString() { + // require(cond, "") yields a empty string + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithOversizedPayload() { + // selector(4) + payload(4097) one byte over the 4096 limit: must be rejected before parse. + byte[] resData = new byte[4101]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNullData() { + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(null)); + } + + @Test + public void testTryDecodeRevertReasonWithShortSelector() { + // length == selector length (4): not enough bytes for any payload, reject. + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(new byte[]{ + 0x08, (byte) 0xc3, 0x79, (byte) 0xa0})); + } + + @Test + public void testTryDecodeRevertReasonWithNonErrorSelector() { + // Non-Error(string) selector (e.g. Panic(uint256) = 0x4e487b71) must be rejected. + byte[] resData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonAtPayloadLimit() { + // selector(4) + payload(4096) exactly at the limit: must go through parse, not size-reject. + byte[] resData = new byte[4100]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + // ABI offset = 0x20 + resData[4 + 31] = 0x20; + // ABI string length = 2 + resData[4 + 32 + 31] = 0x02; + // data "ok", remaining bytes stay zero-padded + resData[4 + 64] = 'o'; + resData[4 + 65] = 'k'; + Assert.assertEquals(": ok", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } +} 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/plugins/build.gradle b/plugins/build.gradle index fc9ef5e00d6..09a13a19b1b 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -39,6 +39,16 @@ dependencies { exclude group: 'io.prometheus' exclude group: 'org.aspectj' exclude group: 'org.apache.httpcomponents' + // x86 declares io.github.tronprotocol:leveldbjni-all:1.18.2 below; + // :crypto -> :common -> :platform also transitively pulls + // org.fusesource.leveldbjni:leveldbjni-all:1.8. Both jars carry + // org/iq80/leveldb/Options.class and the fat jar's last-write-wins + // merge can leave the 1.8 copy, which lacks Options.maxBatchSize(int) + // and breaks `db archive` at runtime. Drop the 1.8 transitive on x86 + // only; ARM64 has a single copy via :platform direct and no conflict. + if (!rootProject.archInfo.isArm64) { + exclude group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all' + } } implementation group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' @@ -49,9 +59,12 @@ dependencies { implementation project(":platform") } else { implementation project(":platform"), { + // Only leveldbjni-all is excluded; the io.github.tronprotocol + // 1.18.2 fork below is the version we want on x86. zksnark-java-sdk + // and commons-io are intentionally kept (the plugins test runtime + // needs both via :crypto -> :common -> :platform and the duplicate + // resolution dedups to one copy). exclude(group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all') - exclude(group: 'io.github.tronprotocol', module: 'zksnark-java-sdk') - exclude(group: 'commons-io', module: 'commons-io') } implementation 'io.github.tronprotocol:leveldbjni-all:1.18.2' implementation 'io.github.tronprotocol:leveldb:1.18.2' @@ -130,7 +143,13 @@ def binaryRelease(taskName, jarName, mainClass) { from(sourceSets.main.output) { include "/**" } - dependsOn (project(':protocol').jar, project(':platform').jar) // explicit_dependency + // Fat jar zips up runtimeClasspath, which includes the jar outputs of + // every project dependency. Declare them all explicitly so Gradle does + // not warn about implicit_dependency and disable execution optimizations + // (and so partial / parallel builds cannot run binaryRelease before the + // dependency jars exist). + dependsOn (project(':protocol').jar, project(':platform').jar, + project(':crypto').jar, project(':common').jar) // explicit_dependency from { configurations.runtimeClasspath.collect { // https://docs.gradle.org/current/userguide/upgrading_version_6.html#changes_6.3 it.isDirectory() ? it : zipTree(it) diff --git a/sprout-verifying.key b/sprout-verifying.key deleted file mode 100644 index 16655abb5d7..00000000000 Binary files a/sprout-verifying.key and /dev/null differ