diff --git a/chainbase/src/main/java/org/tron/common/utils/WalletUtil.java b/chainbase/src/main/java/org/tron/common/utils/WalletUtil.java index 330ce35b6e9..f2d4af266bc 100644 --- a/chainbase/src/main/java/org/tron/common/utils/WalletUtil.java +++ b/chainbase/src/main/java/org/tron/common/utils/WalletUtil.java @@ -8,7 +8,9 @@ import java.util.List; import java.util.stream.Collectors; import org.tron.common.crypto.Hash; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; +import org.tron.core.Constant; import org.tron.core.capsule.ContractCapsule; import org.tron.core.capsule.TransactionCapsule; import org.tron.core.exception.ContractValidateException; @@ -24,6 +26,8 @@ public class WalletUtil { + private static final String INVALID_ADDRESS = "invalid-address"; + public static boolean checkPermissionOperations(Permission permission, Contract contract) throws PermissionException { ByteString operations = permission.getOperations(); @@ -115,9 +119,22 @@ public static boolean isConstant(SmartContract.ABI abi, byte[] selector) { return false; } + public static String getAddressString(byte[] address) { + if (address == null) { + return INVALID_ADDRESS + "(null)"; + } + if (address.length == Constant.STANDARD_ADDRESS_SIZE + || address.length == Constant.TRON_ADDRESS_SIZE) { + return encode58Check(address); + } + int shown = StrictMathWrapper.min(address.length, Constant.TRON_ADDRESS_SIZE); + return INVALID_ADDRESS + "(" + ByteArray.toHexString(Arrays.copyOf(address, shown)) + + (shown < address.length ? "..." : "") + ",len=" + address.length + ")"; + } + public static List getAddressStringList(Collection collection) { return collection.stream() - .map(bytes -> encode58Check(bytes.toByteArray())) + .map(address -> getAddressString(address.toByteArray())) .collect(Collectors.toList()); } diff --git a/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java b/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java index c3776921244..09cfc82570d 100644 --- a/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java +++ b/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java @@ -18,7 +18,6 @@ import org.tron.common.utils.DecodeUtil; import org.tron.common.utils.ForkController; import org.tron.common.utils.Sha256Hash; -import org.tron.common.utils.StringUtil; import org.tron.common.utils.WalletUtil; import org.tron.core.Constant; import org.tron.core.capsule.AccountCapsule; @@ -141,7 +140,7 @@ public void checkIsConstant() throws ContractValidateException, VMIllegalExcepti .get(triggerContractFromTransaction.getContractAddress().toByteArray()); if (contract == null) { throw new ContractValidateException(String.format("contract: %s is not in contract store", - StringUtil.encode58Check(triggerContractFromTransaction + WalletUtil.getAddressString(triggerContractFromTransaction .getContractAddress().toByteArray()))); } diff --git a/common/src/main/java/org/tron/core/exception/InvalidHttpParameterException.java b/common/src/main/java/org/tron/core/exception/InvalidHttpParameterException.java new file mode 100644 index 00000000000..aaf13c50a36 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/InvalidHttpParameterException.java @@ -0,0 +1,15 @@ +package org.tron.core.exception; + +import java.security.InvalidParameterException; + +public class InvalidHttpParameterException extends InvalidParameterException { + + public InvalidHttpParameterException(String message) { + super(message); + } + + public InvalidHttpParameterException(String message, Throwable cause) { + super(message); + initCause(cause); + } +} diff --git a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java index dd8a78a28d6..c1c22d0be17 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java @@ -3,13 +3,12 @@ import com.google.protobuf.InvalidProtocolBufferException; import java.io.IOException; import java.security.SignatureException; -import java.util.stream.Collectors; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.ECKey; import org.tron.common.overlay.message.Message; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; -import org.tron.common.utils.StringUtil; +import org.tron.common.utils.WalletUtil; import org.tron.core.capsule.TransactionCapsule; import org.tron.core.exception.P2pException; import org.tron.protos.Protocol.PBFTMessage; @@ -119,8 +118,7 @@ public String getDataString() { private String decode() { try { SRL srList = SRL.parseFrom(pbftMessage.getRawData().getData().toByteArray()); - return "sr list = " + srList.getSrAddressList().stream().map( - bytes -> StringUtil.encode58Check(bytes.toByteArray())).collect(Collectors.toList()); + return "sr list = " + WalletUtil.getAddressStringList(srList.getSrAddressList()); } catch (InvalidProtocolBufferException e) { } return "decode error"; diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 913d8578216..428a1c6917f 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -118,7 +118,6 @@ import org.tron.common.utils.ByteUtil; import org.tron.common.utils.DecodeUtil; import org.tron.common.utils.Sha256Hash; -import org.tron.common.utils.StringUtil; import org.tron.common.utils.Utils; import org.tron.common.utils.WalletUtil; import org.tron.common.zksnark.IncrementalMerkleTreeContainer; @@ -261,6 +260,9 @@ public class Wallet { "Shielded transaction API is disabled; " + "set node.allowShieldedTransactionApi=true to enable."; private static final String PAYMENT_ADDRESS_FORMAT_WRONG = "paymentAddress format is wrong"; + // the authoritative bound is checkBigIntegerRange: uint256 max is 78 decimal digits, + // this only keeps the string short enough to convert cheaply + private static final int MAX_SHIELDED_AMOUNT_LENGTH = 80; private static final String SHIELDED_TRANSACTION_SCAN_RANGE = "request requires start_block_index >= 0 && end_block_index > " + "start_block_index && end_block_index - start_block_index <= 1000"; @@ -3221,7 +3223,7 @@ public SmartContract getContract(GrpcAPI.BytesMessage bytesMessage) { if (accountCapsule == null) { logger.warn( "Get contract failed, the account {} does not exist or the account " - + "does not have a code hash!", StringUtil.encode58Check(address)); + + "does not have a code hash!", WalletUtil.getAddressString(address)); return null; } @@ -3250,7 +3252,7 @@ public SmartContractDataWrapper getContractInfo(GrpcAPI.BytesMessage bytesMessag if (accountCapsule == null) { logger.warn( "Get contract failed, the account {} does not exist or the account does not have a code " - + "hash!", StringUtil.encode58Check(address)); + + "hash!", WalletUtil.getAddressString(address)); return null; } @@ -4226,6 +4228,9 @@ private BigInteger getBigIntegerFromString(String in) { if (trimmedIn.length() == 0) { return BigInteger.ZERO; } + if (trimmedIn.length() > MAX_SHIELDED_AMOUNT_LENGTH) { + throw new IllegalArgumentException("invalid shielded amount"); + } return new BigInteger(trimmedIn, 10); } @@ -4337,7 +4342,12 @@ public BytesMessage getTriggerInputForShieldedTRC20Contract( ShieldedTRC20Parameters shieldedTRC20Parameters = request.getShieldedTRC20Parameters(); List spendAuthoritySignature = request.getSpendAuthoritySignatureList(); - BigInteger value = getBigIntegerFromString(request.getAmount()); + BigInteger value; + try { + value = getBigIntegerFromString(request.getAmount()); + } catch (IllegalArgumentException e) { + throw new ContractValidateException("invalid amount", e); + } checkBigIntegerRange(value); byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); byte[] transparentToAddressTvm = new byte[20]; @@ -4402,9 +4412,16 @@ public BytesMessage getTriggerInputForShieldedTRC20Contract( } parametersBuilder.setBurnCiphertext(burnCiper); } - String input = parametersBuilder - .getTriggerContractInput(shieldedTRC20Parameters, spendAuthoritySignature, value, false, - transparentToAddressTvm); + String input; + try { + input = parametersBuilder + .getTriggerContractInput(shieldedTRC20Parameters, spendAuthoritySignature, value, false, + transparentToAddressTvm); + } catch (IllegalArgumentException e) { + // the builder names the offending argument in fixed strings; none of them echoes an input + throw new ContractValidateException( + "invalid shielded TRC-20 trigger input: " + e.getMessage(), e); + } if (Objects.isNull(input)) { throw new ZksnarkException("generate the trigger contract parameters error"); } diff --git a/framework/src/main/java/org/tron/core/net/message/handshake/HelloMessage.java b/framework/src/main/java/org/tron/core/net/message/handshake/HelloMessage.java index a36714ede8f..f2be52f006f 100755 --- a/framework/src/main/java/org/tron/core/net/message/handshake/HelloMessage.java +++ b/framework/src/main/java/org/tron/core/net/message/handshake/HelloMessage.java @@ -5,7 +5,7 @@ import org.apache.commons.lang3.StringUtils; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; -import org.tron.common.utils.StringUtil; +import org.tron.common.utils.WalletUtil; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Args; @@ -132,7 +132,7 @@ public String toString() { ByteString address = helloMessage.getAddress(); if (!address.isEmpty()) { builder.append("address:") - .append(StringUtil.encode58Check(address.toByteArray())).append("\n"); + .append(WalletUtil.getAddressString(address.toByteArray())).append("\n"); } ByteString signature = helloMessage.getSignature(); diff --git a/framework/src/main/java/org/tron/core/services/http/servlets/GetBrokerageServlet.java b/framework/src/main/java/org/tron/core/services/http/servlets/GetBrokerageServlet.java index a5d89967da6..8e1380d6f13 100644 --- a/framework/src/main/java/org/tron/core/services/http/servlets/GetBrokerageServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/servlets/GetBrokerageServlet.java @@ -3,7 +3,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.bouncycastle.util.encoders.DecoderException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.core.db.Manager; @@ -21,18 +20,19 @@ public class GetBrokerageServlet extends RateLimiterServlet { private Manager manager; protected void doGet(HttpServletRequest request, HttpServletResponse response) { + byte[] address; + try { + address = Util.getAddress(request); + } catch (Exception e) { + Util.processError(e, response); + return; + } try { - int value = 0; - byte[] address = Util.getAddress(request); long cycle = manager.getDynamicPropertiesStore().getCurrentCycleNumber(); - if (address != null) { - value = manager.getDelegationStore().getBrokerage(cycle, address); - } + int value = manager.getDelegationStore().getBrokerage(cycle, address); response.getWriter().println("{\"brokerage\": " + value + "}"); - } catch (DecoderException | IllegalArgumentException e) { - Util.writeAuditedError(Util.INVALID_ADDRESS_MSG, response); } catch (Exception e) { - Util.processError(e, response); + Util.processServerError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/servlets/GetRewardServlet.java b/framework/src/main/java/org/tron/core/services/http/servlets/GetRewardServlet.java index e3ddccbcb9e..3e0f7599717 100644 --- a/framework/src/main/java/org/tron/core/services/http/servlets/GetRewardServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/servlets/GetRewardServlet.java @@ -3,7 +3,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.bouncycastle.util.encoders.DecoderException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.core.db.Manager; @@ -21,18 +20,19 @@ public class GetRewardServlet extends RateLimiterServlet { private Manager manager; protected void doGet(HttpServletRequest request, HttpServletResponse response) { + byte[] address; try { - long value = 0; - byte[] address = Util.getAddress(request); - if (address != null) { - value = manager.getMortgageService().queryReward(address); - } + address = Util.getAddress(request); + } catch (Exception e) { + Util.processError(e, response); + return; + } + try { + long value = manager.getMortgageService().queryReward(address); String out = JsonFormat.isInt64AsString() ? "{\"reward\": \"" + value + "\"}" : "{\"reward\": " + value + "}"; response.getWriter().println(out); - } catch (DecoderException | IllegalArgumentException e) { - Util.writeAuditedError(Util.INVALID_ADDRESS_MSG, response); } catch (Exception e) { Util.processServerError(e, response); } diff --git a/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java index 1bedc144704..7857ce2c13f 100644 --- a/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java @@ -84,6 +84,8 @@ public class JsonFormat { = "Writing to a StringBuilder threw an IOException (should never happen)."; private static final String EXPECTED_STRING = "Expected string."; private static final String MISSING_END_QUOTE = "String missing ending quote."; + // Keeps the genesis block addresses compatible with their Base58Check rendering. + private static final int MAX_BASE58_ADDRESS_SIZE = 128; // Bounds all work and error messages performed by parseInteger after tokenization. static final int MAX_INTEGER_TOKEN_LENGTH = 256; @@ -889,6 +891,9 @@ static String escapeBytes(ByteString input, final String fieldName, boolean self static String escapeBytesSelfType(ByteString input, final String fieldName) { //Address if (HttpSelfFormatFieldName.isAddressFormat(fieldName)) { + if (input.size() > MAX_BASE58_ADDRESS_SIZE) { + return ByteArray.toHexString(input.toByteArray()); + } return StringUtil.encode58Check(input.toByteArray()); } //Normal String diff --git a/framework/src/main/java/org/tron/core/services/http/servlets/Util.java b/framework/src/main/java/org/tron/core/services/http/servlets/Util.java index 84efdf12e80..234f3687645 100644 --- a/framework/src/main/java/org/tron/core/services/http/servlets/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/servlets/Util.java @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.encoders.DecoderException; import org.bouncycastle.util.encoders.Hex; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.MimeTypes; @@ -40,6 +41,7 @@ import org.tron.common.crypto.Hash; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.DecodeUtil; import org.tron.common.utils.Sha256Hash; import org.tron.core.Constant; import org.tron.core.actuator.TransactionFactory; @@ -48,6 +50,7 @@ import org.tron.core.config.args.Args; import org.tron.core.db.TransactionTrace; import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.InvalidHttpParameterException; import org.tron.core.exception.MaintenanceUnavailableException; import org.tron.core.services.http.servlets.JsonFormat.ParseException; import org.tron.json.JSON; @@ -74,6 +77,10 @@ public class Util { "'events' field is deprecated and no longer supported"; public static final String PERMISSION_ID = "Permission_id"; + private static final String INVALID_PERMISSION_ID = + "invalid " + PERMISSION_ID + ": expect a 32-bit integer"; + private static final int MAX_JSON_INTEGER_VALUE_LENGTH = 64; + private static final String INVALID_JSON_BODY = "INVALID JSON body"; public static final String VISIBLE = "visible"; public static final String INT64_AS_STRING_PARAM = "int64_as_string"; public static final String TRANSACTION = "transaction"; @@ -137,7 +144,8 @@ private static String clientMessage(Exception e) { } if (type == ParseException.class || type == ContractValidateException.class - || type == MaintenanceUnavailableException.class) { + || type == MaintenanceUnavailableException.class + || type == InvalidHttpParameterException.class) { String message = e.getMessage(); return StringUtils.isBlank(message) ? INTERNAL_SERVER_ERROR : message; } @@ -441,14 +449,43 @@ public static String getHexString(final String string) { return ByteArray.toHexString(ByteString.copyFromUtf8(string).toByteArray()); } + /** + * Rejects a json value that cannot be a bounded-length number, before anything converts it. A + * container is not a number and reaches the conversion as its full serialized form, so it has to + * be turned away by type rather than by length. An absent value is left to the caller, which is + * what distinguishes an optional key from a required one. + * + * @throws InvalidHttpParameterException if the value is not a number, or is longer than the bound + */ + private static void checkJsonNumberValue(Object rawValue, String message) { + if (rawValue == null) { + return; + } + if (!(rawValue instanceof String || rawValue instanceof Number) + || rawValue.toString().length() > MAX_JSON_INTEGER_VALUE_LENGTH) { + throw new InvalidHttpParameterException(message); + } + } + public static Transaction setTransactionPermissionId(JSONObject jsonObject, Transaction transaction) { - if (jsonObject.containsKey(PERMISSION_ID)) { - int permissionId = jsonObject.getInteger(PERMISSION_ID); - return setTransactionPermissionId(permissionId, transaction); + if (!jsonObject.containsKey(PERMISSION_ID)) { + return transaction; } - - return transaction; + Object rawValue = jsonObject.get(PERMISSION_ID); + int permissionId; + try { + if (rawValue instanceof Integer) { + permissionId = (Integer) rawValue; + } else if (rawValue instanceof String) { + permissionId = Integer.parseInt((String) rawValue); + } else { + throw new InvalidHttpParameterException(INVALID_PERMISSION_ID); + } + } catch (NumberFormatException e) { + throw new InvalidHttpParameterException(INVALID_PERMISSION_ID, e); + } + return setTransactionPermissionId(permissionId, transaction); } public static Transaction setTransactionPermissionId(int permissionId, Transaction transaction) { @@ -515,11 +552,17 @@ public static long getJsonLongValue(final JSONObject jsonObject, final String ke } public static long getJsonLongValue(JSONObject jsonObject, String key, boolean required) { - BigDecimal bigDecimal = jsonObject.getBigDecimal(key); - if (required && bigDecimal == null) { - throw new InvalidParameterException("key [" + key + "] does not exist"); + String invalid = "invalid key [" + key + "]: expect a 64-bit integer"; + try { + checkJsonNumberValue(jsonObject.get(key), invalid); + BigDecimal bigDecimal = jsonObject.getBigDecimal(key); + if (required && bigDecimal == null) { + throw new InvalidHttpParameterException("key [" + key + "] does not exist"); + } + return (bigDecimal == null) ? 0L : bigDecimal.longValueExact(); + } catch (NumberFormatException | ArithmeticException | JSONException e) { + throw new InvalidHttpParameterException(invalid, e); } - return (bigDecimal == null) ? 0L : bigDecimal.longValueExact(); } public static String getMemo(byte[] memo) { @@ -583,16 +626,42 @@ public static void printAccount(Account reply, HttpServletResponse response, Boo } } + /** + * Returns the address the request carries. An address parameter that is missing or unusable, + * and a json body that cannot be parsed, are reported as an InvalidHttpParameterException whose + * message is a fixed text, so that the address-keyed endpoints and their solidity/PBFT mirrors + * answer a given malformed request identically without each having to classify the failure. + * Nothing derived from the request may go into that message. A query string or form body the + * container itself cannot parse is not classified here and propagates unchanged. + */ public static byte[] getAddress(HttpServletRequest request) throws Exception { - byte[] address = null; String addressParam = "address"; - String addressStr = checkGetParam(request, addressParam); - if (StringUtils.isNotBlank(addressStr)) { - if (StringUtils.startsWith(addressStr, Constant.ADD_PRE_FIX_STRING_MAINNET)) { - address = Hex.decode(addressStr); - } else { - address = decodeFromBase58Check(addressStr); - } + String addressStr; + try { + addressStr = checkGetParam(request, addressParam); + } catch (JSONException e) { + throw new InvalidHttpParameterException(INVALID_JSON_BODY); + } + if (StringUtils.isBlank(addressStr)) { + throw new InvalidHttpParameterException(INVALID_ADDRESS_MSG); + } + + boolean isHex = StringUtils.startsWith(addressStr, Constant.ADD_PRE_FIX_STRING_MAINNET); + // bound the hex input before decoding, mirroring the base58 length short-circuit + if (isHex && addressStr.length() != DecodeUtil.ADDRESS_SIZE) { + throw new InvalidHttpParameterException(INVALID_ADDRESS_MSG); + } + + byte[] address; + try { + address = isHex ? Hex.decode(addressStr) : decodeFromBase58Check(addressStr); + } catch (DecoderException | IllegalArgumentException exception) { + // both decoders name the offending character and its offset, which is caller input + throw new InvalidHttpParameterException(INVALID_ADDRESS_MSG); + } + // base58 is validated inside the decoder; hex used to be returned unchecked + if (address == null || (isHex && !DecodeUtil.addressValid(address))) { + throw new InvalidHttpParameterException(INVALID_ADDRESS_MSG); } return address; } 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 4bef932971d..a3e44187b2c 100644 --- a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java @@ -402,6 +402,9 @@ private String mintParamsToHexString(GrpcAPI.ShieldedTRC20Parameters mintParams, if (value.compareTo(BigInteger.ZERO) <= 0) { throw new IllegalArgumentException("require the value be positive"); } + if (mintParams.getReceiveDescriptionCount() != 1) { + throw new IllegalArgumentException("invalid mint output number"); + } ShieldContract.ReceiveDescription revDesc = mintParams.getReceiveDescription(0); byte[] zeros = new byte[12]; @@ -422,12 +425,22 @@ private String mintParamsToHexString(GrpcAPI.ShieldedTRC20Parameters mintParams, private String transferParamsToHexString(GrpcAPI.ShieldedTRC20Parameters transferParams, List spendAuthoritySignature, boolean withAsk) { + List spendDescs = transferParams.getSpendDescriptionList(); + List recvDescs = transferParams.getReceiveDescriptionList(); + long spendCount = spendDescs.size(); + long recvCount = recvDescs.size(); + if (spendCount != 1 && spendCount != 2) { + throw new IllegalArgumentException("invalid transfer input number"); + } + if (recvCount != 1 && recvCount != 2) { + throw new IllegalArgumentException("invalid transfer output number"); + } + byte[] input = new byte[0]; byte[] spendAuthSig = new byte[0]; byte[] output = new byte[0]; byte[] c = new byte[0]; byte[] bindingSig; - List spendDescs = transferParams.getSpendDescriptionList(); for (ShieldContract.SpendDescription spendDesc : spendDescs) { input = ByteUtil.merge(input, spendDesc.getNullifier().toByteArray(), @@ -441,10 +454,6 @@ private String transferParamsToHexString(GrpcAPI.ShieldedTRC20Parameters transfe spendAuthSig, spendDesc.getSpendAuthoritySignature().toByteArray()); } } - long spendCount = spendDescs.size(); - if (spendCount < 1 || spendCount > 2) { - throw new IllegalArgumentException("invalid transfer input number"); - } if (!withAsk) { if (spendCount == 1) { spendAuthSig = spendAuthoritySignature.get(0).getValue().toByteArray(); @@ -458,7 +467,6 @@ private String transferParamsToHexString(GrpcAPI.ShieldedTRC20Parameters transfe byte[] spendCountBytes = ByteUtil.longTo32Bytes(spendCount); byte[] authOffsetBytes = ByteUtil.longTo32Bytes(192 + 32 + 320 * spendCount); - List recvDescs = transferParams.getReceiveDescriptionList(); for (ShieldContract.ReceiveDescription recvDesc : recvDescs) { output = ByteUtil.merge(output, recvDesc.getNoteCommitment().toByteArray(), @@ -474,7 +482,6 @@ private String transferParamsToHexString(GrpcAPI.ShieldedTRC20Parameters transfe ); } - long recvCount = recvDescs.size(); byte[] recvCountBytes = ByteUtil.longTo32Bytes(recvCount); byte[] outputOffsetbytes = ByteUtil .longTo32Bytes(192 + 32 + 320 * spendCount + 32 + 64 * spendCount); @@ -508,6 +515,9 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, if (value.compareTo(BigInteger.ZERO) <= 0) { throw new IllegalArgumentException("the value must be positive"); } + if (burnParams.getReceiveDescriptionCount() > 1) { + throw new IllegalArgumentException("invalid burn output number"); + } byte[] transparentToAddressTvm = normalizeTransparentToAddress(transparentToAddress); diff --git a/framework/src/test/java/org/tron/common/utils/WalletUtilTest.java b/framework/src/test/java/org/tron/common/utils/WalletUtilTest.java new file mode 100644 index 00000000000..db1e7b311b1 --- /dev/null +++ b/framework/src/test/java/org/tron/common/utils/WalletUtilTest.java @@ -0,0 +1,72 @@ +package org.tron.common.utils; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mockStatic; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import java.util.Collections; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class WalletUtilTest { + + @Test + public void encodesAddressSizesAsBase58AndDoesNotModifyInput() { + int[] sizes = {20, 21}; + String[] expected = {"6x36TdFVhCQBxJz1MzEoySdejiLcwZMed", + "TFvF6WzyeRKMM8NcbddxL88mZXTeuUWA8Z"}; + for (int i = 0; i < sizes.length; i++) { + byte[] input = bytes(sizes[i]); + byte[] original = input.clone(); + assertEquals(expected[i], StringUtil.encode58Check(input)); + assertEquals(expected[i], WalletUtil.getAddressString(input)); + assertArrayEquals(original, input); + } + } + + @Test + public void formatsShortInputsAsFullHex() { + assertEquals("invalid-address(,len=0)", WalletUtil.getAddressString(bytes(0))); + assertEquals("invalid-address(41,len=1)", WalletUtil.getAddressString(bytes(1))); + assertEquals("invalid-address(" + StringUtils.repeat("41", 19) + ",len=19)", + WalletUtil.getAddressString(bytes(19))); + } + + @Test + public void formatsNullAsInvalidAddress() { + assertEquals("invalid-address(null)", WalletUtil.getAddressString(null)); + } + + @Test + public void formatsOversizedInputsWithoutHashingOrBase58Conversion() { + for (int size : new int[]{22, 4096, 16384, 65536}) { + byte[] input = bytes(size); + try (MockedStatic hash = mockStatic(Sha256Hash.class); + MockedStatic base58 = mockStatic(Base58.class)) { + String expected = "invalid-address(" + StringUtils.repeat("41", 21) + + "...,len=" + size + ")"; + assertEquals(expected, WalletUtil.getAddressString(input)); + hash.verifyNoInteractions(); + base58.verifyNoInteractions(); + } + } + } + + @Test + public void preservesOrdinaryWitnessLists() { + ByteString address = ByteString.copyFrom(bytes(21)); + assertEquals(Collections.nCopies(27, StringUtil.encode58Check(address.toByteArray())), + WalletUtil.getAddressStringList(Collections.nCopies(27, address))); + assertTrue(WalletUtil.getAddressStringList(Collections.emptyList()).isEmpty()); + } + + private static byte[] bytes(int size) { + byte[] input = new byte[size]; + Arrays.fill(input, (byte) 0x41); + return input; + } +} diff --git a/framework/src/test/java/org/tron/core/WalletContractAddressLoggingTest.java b/framework/src/test/java/org/tron/core/WalletContractAddressLoggingTest.java new file mode 100644 index 00000000000..7f2ca3094b4 --- /dev/null +++ b/framework/src/test/java/org/tron/core/WalletContractAddressLoggingTest.java @@ -0,0 +1,41 @@ +package org.tron.core; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import java.lang.reflect.Field; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.api.GrpcAPI.BytesMessage; +import org.tron.common.utils.Base58; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.store.AccountStore; + +public class WalletContractAddressLoggingTest { + + @Test + public void oversizedMissingAddressIsLoggedWithoutBase58() throws Exception { + Wallet wallet = new Wallet(); + ChainBaseManager chainBaseManager = mock(ChainBaseManager.class); + AccountStore accountStore = mock(AccountStore.class); + when(accountStore.get(any(byte[].class))).thenReturn((AccountCapsule) null); + when(chainBaseManager.getAccountStore()).thenReturn(accountStore); + Field field = Wallet.class.getDeclaredField("chainBaseManager"); + field.setAccessible(true); + field.set(wallet, chainBaseManager); + + for (int size : new int[] {22, 32 * 1024}) { + BytesMessage message = BytesMessage.newBuilder() + .setValue(ByteString.copyFrom(new byte[size])).build(); + try (MockedStatic base58 = mockStatic(Base58.class)) { + assertNull(wallet.getContract(message)); + assertNull(wallet.getContractInfo(message)); + base58.verifyNoInteractions(); + } + } + } +} diff --git a/framework/src/test/java/org/tron/core/WalletMockTest.java b/framework/src/test/java/org/tron/core/WalletMockTest.java index a107f57520f..4a624f11862 100644 --- a/framework/src/test/java/org/tron/core/WalletMockTest.java +++ b/framework/src/test/java/org/tron/core/WalletMockTest.java @@ -953,6 +953,7 @@ public void testGetTriggerInputForShieldedTRC20Contract1() GrpcAPI.ShieldedTRC20Parameters shieldedTRC20Parameters = GrpcAPI.ShieldedTRC20Parameters.newBuilder() .addSpendDescription(spendDescription) + .addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()) .setParameterType("transfer") .build(); GrpcAPI.BytesMessage bytesMessage = diff --git a/framework/src/test/java/org/tron/core/WalletShieldedAmountValidationTest.java b/framework/src/test/java/org/tron/core/WalletShieldedAmountValidationTest.java new file mode 100644 index 00000000000..58c65a71449 --- /dev/null +++ b/framework/src/test/java/org/tron/core/WalletShieldedAmountValidationTest.java @@ -0,0 +1,148 @@ +package org.tron.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigInteger; +import org.apache.commons.lang3.StringUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.api.GrpcAPI; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ZksnarkException; + +public class WalletShieldedAmountValidationTest { + + // both methods under test are pure and never touch instance state, so one wallet is enough; + // constructing one generates a keypair, which is not worth paying for on every invocation + private static Wallet wallet; + private static Method bigIntegerFromString; + private static Method checkBigIntegerRange; + + @BeforeClass + public static void setUpClass() throws Exception { + wallet = new Wallet(); + bigIntegerFromString = + Wallet.class.getDeclaredMethod("getBigIntegerFromString", String.class); + bigIntegerFromString.setAccessible(true); + checkBigIntegerRange = + Wallet.class.getDeclaredMethod("checkBigIntegerRange", BigInteger.class); + checkBigIntegerRange.setAccessible(true); + } + + private static BigInteger parse(String value) throws Exception { + try { + return (BigInteger) bigIntegerFromString.invoke(wallet, value); + } catch (InvocationTargetException exception) { + throw (Exception) exception.getCause(); + } + } + + private static void checkRange(BigInteger value) throws Exception { + try { + checkBigIntegerRange.invoke(wallet, value); + } catch (InvocationTargetException exception) { + throw (Exception) exception.getCause(); + } + } + + @Test + public void amountLengthBoundaryIsAppliedAfterTrimAndBeforeParsing() throws Exception { + assertEquals(BigInteger.ZERO, parse(" ")); + assertEquals(BigInteger.ZERO, parse(" " + StringUtils.repeat('0', 80) + " ")); + + String oversized = StringUtils.repeat('9', 81); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> parse(oversized)); + assertEquals("invalid shielded amount", exception.getMessage()); + assertFalse(exception.getMessage().contains(oversized)); + assertThrows(IllegalArgumentException.class, () -> parse(StringUtils.repeat('0', 81))); + } + + @Test + public void uint256AndSignRangeChecksRemainAuthoritative() throws Exception { + BigInteger maximum = BigInteger.ONE.shiftLeft(256).subtract(BigInteger.ONE); + assertEquals(maximum, parse(maximum.toString())); + checkRange(maximum); + + ContractValidateException tooLarge = assertThrows(ContractValidateException.class, + () -> checkRange(parse(BigInteger.ONE.shiftLeft(256).toString()))); + assertTrue(tooLarge.getMessage().contains("256 bits")); + + ContractValidateException negative = assertThrows(ContractValidateException.class, + () -> checkRange(parse("-1"))); + assertTrue(negative.getMessage().contains("non-negative")); + assertEquals(BigInteger.valueOf(123), parse(" 123 ")); + } + + /** Runs the public entrypoint with the shielded api enabled, so the amount is reached. */ + private static void triggerInputWithAmount(String amount) throws Exception { + GrpcAPI.ShieldedTRC20TriggerContractParameters request = + GrpcAPI.ShieldedTRC20TriggerContractParameters.newBuilder() + .setAmount(amount) + .setShieldedTRC20Parameters(GrpcAPI.ShieldedTRC20Parameters.newBuilder() + .setParameterType("transfer") + .build()) + .build(); + CommonParameter commonParameter = mock(Args.class); + try (MockedStatic mocked = mockStatic(CommonParameter.class)) { + when(CommonParameter.getInstance()).thenReturn(commonParameter); + when(commonParameter.isAllowShieldedTransactionApi()).thenReturn(true); + wallet.getTriggerInputForShieldedTRC20Contract(request); + } + } + + /** + * getBigIntegerFromString reports an overlong amount, and a malformed one, with unchecked + * exceptions. The entrypoint declares only ZksnarkException and ContractValidateException, and + * the http error sanitizer answers an untranslated failure with "internal server error" instead + * of the validation error every other amount entrypoint answers with. + */ + @Test + public void unusableAmountsAreReportedAsValidationFailures() { + String[] unusable = {StringUtils.repeat('9', 81), "not-a-number", "1.5"}; + for (String amount : unusable) { + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> triggerInputWithAmount(amount)); + assertEquals("invalid amount", exception.getMessage()); + } + } + + /** A well-formed amount past the uint256 range still reports the range failure, not "invalid". */ + @Test + public void outOfRangeAmountRetainsRangeValidationMessage() { + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> triggerInputWithAmount(BigInteger.ONE.shiftLeft(256).toString())); + assertTrue(exception.getMessage().contains("256 bits")); + } + + @Test + public void disabledFeatureGateRunsBeforeAmountOrDescriptionValidation() { + GrpcAPI.ShieldedTRC20TriggerContractParameters request = + GrpcAPI.ShieldedTRC20TriggerContractParameters.newBuilder() + .setAmount(StringUtils.repeat('9', 81)) + .setShieldedTRC20Parameters(GrpcAPI.ShieldedTRC20Parameters.newBuilder() + .setParameterType("transfer") + .build()) + .build(); + CommonParameter commonParameter = mock(Args.class); + try (MockedStatic mocked = mockStatic(CommonParameter.class)) { + when(CommonParameter.getInstance()).thenReturn(commonParameter); + when(commonParameter.isAllowShieldedTransactionApi()).thenReturn(false); + + ZksnarkException exception = assertThrows(ZksnarkException.class, + () -> wallet.getTriggerInputForShieldedTRC20Contract(request)); + assertTrue(exception.getMessage().contains("Shielded transaction API is disabled")); + } + } +} diff --git a/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java b/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java new file mode 100644 index 00000000000..a40dbc105c5 --- /dev/null +++ b/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java @@ -0,0 +1,124 @@ +package org.tron.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import java.util.Arrays; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.api.GrpcAPI; +import org.tron.api.GrpcAPI.BytesMessage; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.ByteUtil; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.zen.note.NoteEncryption.Encryption; +import org.tron.protos.contract.ShieldContract; + +public class WalletShieldedDescriptionValidationTest { + + private static Wallet wallet; + + @BeforeClass + public static void setUpClass() { + wallet = new Wallet(); + } + + private static BytesMessage triggerInput(GrpcAPI.ShieldedTRC20Parameters parameters, + String amount, int signatureCount) throws Exception { + GrpcAPI.ShieldedTRC20TriggerContractParameters.Builder request = + GrpcAPI.ShieldedTRC20TriggerContractParameters.newBuilder() + .setAmount(amount) + .setShieldedTRC20Parameters(parameters); + for (int i = 0; i < signatureCount; i++) { + request.addSpendAuthoritySignature(BytesMessage.getDefaultInstance()); + } + + CommonParameter commonParameter = mock(Args.class); + try (MockedStatic mocked = mockStatic(CommonParameter.class)) { + when(CommonParameter.getInstance()).thenReturn(commonParameter); + when(commonParameter.isAllowShieldedTransactionApi()).thenReturn(true); + return wallet.getTriggerInputForShieldedTRC20Contract(request.build()); + } + } + + private static GrpcAPI.ShieldedTRC20Parameters withReceives( + GrpcAPI.ShieldedTRC20Parameters.Builder parameters, int receives) { + for (int i = 0; i < receives; i++) { + parameters.addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()); + } + return parameters.build(); + } + + /** + * A burn whose v2 record carries the marker and the nonce bound to (nullifier, amount, zero + * transparent address), so the request clears the existing record checks. + */ + private static GrpcAPI.ShieldedTRC20Parameters burn(int receives, String amount) { + byte[] nullifier = new byte[32]; + Arrays.fill(nullifier, (byte) 1); + byte[] addr21 = new byte[21]; + addr21[0] = Wallet.getAddressPreFixByte(); + byte[] nonce = Encryption.deriveBurnNonce(nullifier, + ByteUtil.bigIntegerToBytes(new BigInteger(amount), 32), addr21); + byte[] record = new byte[Encryption.BURN_CIPHER_RECORD_SIZE]; + System.arraycopy(nonce, 0, record, Encryption.BURN_NONCE_OFFSET, Encryption.BURN_NONCE_LEN); + System.arraycopy(Encryption.getBurnRecordV2Marker(), 0, record, + Encryption.BURN_RESERVED_OFFSET, Encryption.BURN_RESERVED_LEN); + return withReceives(GrpcAPI.ShieldedTRC20Parameters.newBuilder() + .setParameterType("burn") + .setTriggerContractInput(ByteArray.toHexString(record)) + .addSpendDescription(ShieldContract.SpendDescription.newBuilder() + .setNullifier(ByteString.copyFrom(nullifier))), receives); + } + + @Test + public void transferDescriptionCardinalityIsReportedAsValidationFailure() { + GrpcAPI.ShieldedTRC20Parameters.Builder parameters = + GrpcAPI.ShieldedTRC20Parameters.newBuilder() + .setParameterType("transfer") + .addSpendDescription(ShieldContract.SpendDescription.getDefaultInstance()); + for (int i = 0; i < 3; i++) { + parameters.addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()); + } + + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> triggerInput(parameters.build(), "0", 1)); + assertEquals("invalid shielded TRC-20 trigger input: invalid transfer output number", + exception.getMessage()); + } + + @Test + public void mintRequiresExactlyOneReceiveDescription() throws Exception { + for (int receives : new int[]{0, 2}) { + GrpcAPI.ShieldedTRC20Parameters parameters = withReceives( + GrpcAPI.ShieldedTRC20Parameters.newBuilder().setParameterType("mint"), receives); + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> triggerInput(parameters, "1", 0)); + assertEquals("invalid shielded TRC-20 trigger input: invalid mint output number", + exception.getMessage()); + } + assertFalse(triggerInput(withReceives( + GrpcAPI.ShieldedTRC20Parameters.newBuilder().setParameterType("mint"), 1), "1", 0) + .getValue().isEmpty()); + } + + @Test + public void burnAcceptsAtMostOneReceiveDescription() throws Exception { + assertFalse(triggerInput(burn(0, "1"), "1", 1).getValue().isEmpty()); + assertFalse(triggerInput(burn(1, "1"), "1", 1).getValue().isEmpty()); + + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> triggerInput(burn(2, "1"), "1", 1)); + assertEquals("invalid shielded TRC-20 trigger input: invalid burn output number", + exception.getMessage()); + } +} diff --git a/framework/src/test/java/org/tron/core/db/Base58ValidationErrorTest.java b/framework/src/test/java/org/tron/core/db/Base58ValidationErrorTest.java new file mode 100644 index 00000000000..2db0a0c68f0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/Base58ValidationErrorTest.java @@ -0,0 +1,53 @@ +package org.tron.core.db; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.common.runtime.Runtime; +import org.tron.common.utils.Base58; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.store.AccountStore; +import org.tron.core.store.ContractStore; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.StoreFactory; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; + +public class Base58ValidationErrorTest { + + @Test + public void missingContractKeepsContractValidationExceptionAndConstantinopleGate() + throws Exception { + ChainBaseManager manager = mock(ChainBaseManager.class); + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + when(properties.getLatestVersion()).thenReturn(1); + when(manager.getDynamicPropertiesStore()).thenReturn(properties); + when(manager.getContractStore()).thenReturn(mock(ContractStore.class)); + when(manager.getAccountStore()).thenReturn(mock(AccountStore.class)); + StoreFactory stores = mock(StoreFactory.class); + when(stores.getChainBaseManager()).thenReturn(manager); + TransactionCapsule transaction = new TransactionCapsule(TriggerSmartContract.newBuilder() + .setContractAddress(ByteString.copyFrom(new byte[16384])).build(), + ContractType.TriggerSmartContract); + TransactionTrace trace = new TransactionTrace(transaction, stores, mock(Runtime.class)); + try (MockedStatic base58 = mockStatic(Base58.class)) { + ContractValidateException failure = assertThrows(ContractValidateException.class, + trace::checkIsConstant); + assertTrue(failure.getMessage(), + failure.getMessage().contains( + "invalid-address(" + StringUtils.repeat("00", 21) + "...,len=16384)")); + when(properties.getAllowTvmConstantinople()).thenReturn(1L); + trace.checkIsConstant(); + base58.verifyNoInteractions(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/net/Base58MessageLoggingTest.java b/framework/src/test/java/org/tron/core/net/Base58MessageLoggingTest.java new file mode 100644 index 00000000000..8bc34312e26 --- /dev/null +++ b/framework/src/test/java/org/tron/core/net/Base58MessageLoggingTest.java @@ -0,0 +1,222 @@ +package org.tron.core.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.google.protobuf.ByteString; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.slf4j.LoggerFactory; +import org.tron.common.overlay.message.Message; +import org.tron.common.utils.Base58; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.StringUtil; +import org.tron.consensus.base.Param; +import org.tron.consensus.base.PbftInterface; +import org.tron.consensus.pbft.message.PbftMessage; +import org.tron.core.net.message.MessageTypes; +import org.tron.core.net.message.handshake.HelloMessage; +import org.tron.core.net.messagehandler.PbftMsgHandler; +import org.tron.core.net.peer.PeerConnection; +import org.tron.core.net.peer.PeerManager; +import org.tron.core.net.service.handshake.HandshakeService; +import org.tron.core.net.service.statistics.PeerStatistics; +import org.tron.p2p.P2pConfig; +import org.tron.p2p.P2pService; +import org.tron.p2p.base.Parameter; +import org.tron.p2p.connection.Channel; +import org.tron.protos.Discover; +import org.tron.protos.Protocol; +import org.tron.protos.Protocol.PBFTMessage; +import org.tron.protos.Protocol.ReasonCode; + +public class Base58MessageLoggingTest { + + private final Logger logger = (Logger) LoggerFactory.getLogger("net"); + private final List logMessages = new ArrayList<>(); + private final Thread testThread = Thread.currentThread(); + private Level oldLevel; + private P2pConfig oldConfig; + private PbftInterface oldPbftInterface; + private AppenderBase appender; + + @Before + public void setUp() { + oldLevel = logger.getLevel(); + logger.setLevel(Level.INFO); + appender = new AppenderBase() { + @Override + protected void append(ILoggingEvent event) { + // Background threads left by other tests in the same JVM also log to "net". + if (Thread.currentThread() != testThread) { + return; + } + // Force formatting inside the receive path, as a normal logging appender does. + logMessages.add(event.getFormattedMessage()); + } + }; + appender.start(); + logger.addAppender(appender); + oldConfig = Parameter.p2pConfig; + Parameter.p2pConfig = new P2pConfig(); + Parameter.p2pConfig.setIp("127.0.0.1"); + oldPbftInterface = Param.getInstance().getPbftInterface(); + Param.getInstance().setPbftInterface(mock(PbftInterface.class)); + } + + @After + public void tearDown() { + logger.detachAppender(appender); + appender.stop(); + logger.setLevel(oldLevel); + Parameter.p2pConfig = oldConfig; + Param.getInstance().setPbftInterface(oldPbftInterface); + } + + @Test + public void helloReceiveLogCannotEncodeOversizedAddressBeforeHandshakeRejection() + throws Exception { + for (Level level : new Level[]{Level.INFO, Level.OFF}) { + logger.setLevel(level); + logMessages.clear(); + PeerConnection peer = peer(); + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + setField(handler, "handshakeService", new HandshakeService()); + Method process = P2pEventHandlerImpl.class.getDeclaredMethod("processMessage", + PeerConnection.class, byte[].class); + process.setAccessible(true); + HelloMessage message = hello(16384); + try (MockedStatic network = mockStatic(TronNetService.class); + MockedStatic base58 = mockStatic(Base58.class)) { + network.when(TronNetService::getP2pService).thenReturn(mock(P2pService.class)); + process.invoke(handler, peer, message.getSendBytes()); + verify(peer).disconnect(ReasonCode.INCOMPATIBLE_PROTOCOL); + base58.verifyNoInteractions(); + } + if (level == Level.INFO) { + assertTrue(logMessages.stream().anyMatch(s -> s.contains(",len=16384)"))); + } + } + } + + @Test + public void helloFormattingPreservesSmallInputsAndExistingLengthValidation() throws Exception { + HelloMessage empty = hello(0); + assertTrue(empty.valid()); + assertFalse(empty.toString().contains("address:")); + HelloMessage normal = hello(21); + assertTrue(normal.valid()); + assertTrue(normal.toString().contains(StringUtil.encode58Check(bytes(21).toByteArray()))); + HelloMessage unusual = hello(200); + assertTrue(unusual.valid()); + assertTrue(unusual.toString().contains(",len=200)")); + assertFalse(hello(201).valid()); + } + + @Test + public void pbftSignatureFailureLogIsSafeAndDisabledPbftStillReturnsEarly() throws Exception { + PbftMessage message = pbft(Protocol.SRL.newBuilder() + .addSrAddress(bytes(16384)).build().toByteString()); + for (boolean enabled : new boolean[]{false, true}) { + PeerConnection peer = peer(); + Channel channel = peer.getChannel(); + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + PbftMsgHandler pbftHandler = new PbftMsgHandler(); + TronNetDelegate delegate = mock(TronNetDelegate.class); + when(delegate.allowPBFT()).thenReturn(enabled); + setField(pbftHandler, "tronNetDelegate", delegate); + setField(handler, "pbftMsgHandler", pbftHandler); + try (MockedStatic peers = mockStatic(PeerManager.class); + MockedStatic messages = mockStatic(Message.class, CALLS_REAL_METHODS); + MockedStatic base58 = mockStatic(Base58.class)) { + peers.when(() -> PeerManager.getPeerConnection(channel)).thenReturn(peer); + messages.when(Message::isFilter).thenReturn(false); + logMessages.clear(); + handler.onMessage(channel, message.getSendBytes()); + if (enabled) { + verify(peer).disconnect(ReasonCode.BAD_PROTOCOL); + assertTrue(logMessages.stream().anyMatch(s -> s.contains(",len=16384)"))); + } else { + verify(peer, never()).disconnect(ReasonCode.BAD_PROTOCOL); + assertTrue(logMessages.toString(), logMessages.isEmpty()); + } + base58.verifyNoInteractions(); + } + } + } + + @Test + public void formatsSrlListAndHandlesMalformedSrl() { + ByteString address = bytes(21); + PbftMessage ordinary = pbft(Protocol.SRL.newBuilder() + .addAllSrAddress(Collections.nCopies(27, address)).build().toByteString()); + assertEquals("sr list = " + + Collections.nCopies(27, StringUtil.encode58Check(address.toByteArray())), + ordinary.getDataString()); + assertEquals("decode error", pbft(ByteString.copyFrom(new byte[]{-1})).getDataString()); + } + + private static PeerConnection peer() { + PeerConnection peer = mock(PeerConnection.class); + when(peer.getPeerStatistics()).thenReturn(new PeerStatistics()); + Channel channel = mock(Channel.class); + when(channel.getNodeId()).thenReturn(ByteArray.toHexString(new byte[64])); + when(peer.getChannel()).thenReturn(channel); + when(peer.getInetSocketAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 18888)); + return peer; + } + + private static HelloMessage hello(int size) throws Exception { + Protocol.HelloMessage.BlockId block = Protocol.HelloMessage.BlockId.newBuilder() + .setHash(ByteString.copyFrom(new byte[32])).build(); + Protocol.HelloMessage value = Protocol.HelloMessage.newBuilder() + .setFrom(Discover.Endpoint.newBuilder().setAddress(ByteString.copyFromUtf8("127.0.0.1")) + .setPort(18888).setNodeId(ByteString.copyFrom(new byte[64]))) + .setGenesisBlockId(block).setSolidBlockId(block).setHeadBlockId(block) + .setAddress(bytes(size)).build(); + return new HelloMessage(value.toByteArray()); + } + + private static PbftMessage pbft(ByteString data) { + PbftMessage message = new PbftMessage(); + PBFTMessage value = PBFTMessage.newBuilder().setRawData(PBFTMessage.Raw.newBuilder() + .setDataType(PBFTMessage.DataType.SRL).setMsgType(PBFTMessage.MsgType.PREPREPARE) + .setData(data)).build(); + message.setPbftMessage(value); + message.setData(value.toByteArray()); + message.setType(MessageTypes.PBFT_MSG.asByte()); + return message; + } + + private static ByteString bytes(int size) { + byte[] value = new byte[size]; + Arrays.fill(value, (byte) 0x41); + return ByteString.copyFrom(value); + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/AddressQueryServletTestBase.java b/framework/src/test/java/org/tron/core/services/http/servlets/AddressQueryServletTestBase.java new file mode 100644 index 00000000000..b5642ddac67 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/servlets/AddressQueryServletTestBase.java @@ -0,0 +1,184 @@ +package org.tron.core.services.http.servlets; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.Arrays; +import java.util.Collections; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +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.utils.StringUtil; +import org.tron.core.Wallet; +import org.tron.json.JSON; +import org.tron.json.JSONObject; + +/** + * The shared address-parameter contract for the reward and brokerage endpoints. The cases live + * here rather than in each servlet's test so that the endpoints cannot drift apart in how they + * answer a given malformed request. + */ +public abstract class AddressQueryServletTestBase extends BaseTest { + + static final String INVALID_ADDRESS_ERROR = "INVALID address"; + + /** Invokes the concrete servlet's GET handler. */ + protected abstract void invokeGet(MockHttpServletRequest request, + MockHttpServletResponse response); + + /** Invokes the concrete servlet's POST handler. */ + protected abstract void invokePost(MockHttpServletRequest request, + MockHttpServletResponse response); + + /** The key this endpoint answers with, for instance {@code reward}. */ + protected abstract String valueKey(); + + /** What this endpoint answers for a well-formed address it holds no state for. */ + protected abstract int valueWithoutState(); + + protected static MockHttpServletRequest postRequest(String contentType) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType(contentType); + request.setCharacterEncoding(UTF_8.name()); + return request; + } + + protected static MockHttpServletRequest getRequest(String address) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + if (address != null) { + request.addParameter("address", address); + } + return request; + } + + protected static MockHttpServletRequest formRequest(String address) { + MockHttpServletRequest request = postRequest("application/x-www-form-urlencoded"); + if (address != null) { + request.addParameter("address", address); + } + return request; + } + + protected static MockHttpServletRequest jsonRequest(String address) { + return jsonRequest(address, "application/json"); + } + + protected static MockHttpServletRequest jsonRequest(String address, String contentType) { + MockHttpServletRequest request = postRequest(contentType); + String json = address == null ? "{}" + : JSON.toJSONString(Collections.singletonMap("address", address)); + request.setContent(json.getBytes(UTF_8)); + return request; + } + + /** A base58check payload whose first byte is not the address prefix. */ + protected static String wrongPrefixAddress() { + byte[] address = new byte[21]; + address[0] = (byte) (Wallet.getAddressPreFixByte() + 1); + return StringUtil.encode58Check(address); + } + + /** A well-formed address that no store holds anything for. */ + protected static String canonicalNoStateAddress() { + byte[] address = new byte[21]; + Arrays.fill(address, (byte) 7); + address[0] = Wallet.getAddressPreFixByte(); + return StringUtil.encode58Check(address); + } + + /** Runs the request through the servlet with the verb it carries, and returns the raw body. */ + protected String invoke(MockHttpServletRequest request) throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + if ("GET".equals(request.getMethod())) { + invokeGet(request, response); + } else { + invokePost(request, response); + } + return response.getContentAsString(); + } + + private void assertInvalid(MockHttpServletRequest request, String submitted) throws Exception { + String body = invoke(request); + JSONObject result = JSONObject.parseObject(body); + Assert.assertEquals(INVALID_ADDRESS_ERROR, result.get("Error")); + Assert.assertNull(result.get(valueKey())); + if (submitted != null && !submitted.isEmpty()) { + Assert.assertFalse("the error must not echo the submitted address", body.contains(submitted)); + } + } + + private void assertValue(MockHttpServletRequest request, int expected) throws Exception { + JSONObject result = JSONObject.parseObject(invoke(request)); + Assert.assertEquals(expected, (int) result.get(valueKey())); + Assert.assertNull(result.get("Error")); + } + + @Test + public void getAndPostRejectMalformedAddresses() throws Exception { + String invalidBase58 = "Taaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + // '0' is outside the base58 alphabet, so the decoder itself raises + String illegalBase58Char = "Taaaaaaaaaaaaaaaaa0aaaaaaaaaaaaaaa"; + String invalidHex = "41zz00000000000000000000000000000000000000"; + // unlike a raw "42..." string this reaches the prefix check instead of failing the alphabet + String wrongPrefix = wrongPrefixAddress(); + String oversizedHex = "41" + StringUtils.repeat('0', 8192); + String oversizedBase58 = "T" + StringUtils.repeat('a', 8192); + + assertInvalid(getRequest(invalidBase58), invalidBase58); + assertInvalid(getRequest(illegalBase58Char), illegalBase58Char); + assertInvalid(getRequest(wrongPrefix), wrongPrefix); + assertInvalid(getRequest(oversizedHex), oversizedHex); + assertInvalid(getRequest(oversizedBase58), oversizedBase58); + assertInvalid(jsonRequest(oversizedBase58), oversizedBase58); + assertInvalid(jsonRequest(invalidBase58), invalidBase58); + assertInvalid(jsonRequest(invalidHex), invalidHex); + } + + /** + * An address that is absent or blank is rejected like any other address the endpoint cannot + * use, on every verb and body encoding. It used to answer with the service default instead. + */ + @Test + public void missingOrBlankAddressIsRejected() throws Exception { + assertInvalid(getRequest(null), null); + assertInvalid(getRequest(""), null); + assertInvalid(formRequest(null), null); + assertInvalid(formRequest(""), null); + assertInvalid(jsonRequest(null), null); + assertInvalid(jsonRequest(""), null); + } + + @Test + public void validAddressWithoutStateRetainsServiceDefault() throws Exception { + assertValue(getRequest(canonicalNoStateAddress()), valueWithoutState()); + } + + /** + * The charset-suffixed content type is what browsers actually send, and it must take the + * json-body branch of Util.checkGetParam rather than the form-parameter branch. The body + * carries a well-formed address: on the form branch no address would be found at all and + * the answer would be a rejection instead of the value. + */ + @Test + public void jsonBodyWithCharsetSuffixIsParsed() throws Exception { + assertValue(jsonRequest(canonicalNoStateAddress(), "application/json; charset=utf-8"), + valueWithoutState()); + } + + @Test + public void malformedJsonBodyIsRejectedWithoutEchoingIt() throws Exception { + String marker = "UniqueMarkerValue"; + MockHttpServletRequest request = postRequest("application/json"); + request.setContent(("{\"address\":" + marker + "}").getBytes(UTF_8)); + + String body = invoke(request); + Assert.assertFalse(body.contains(marker)); + JSONObject result = JSONObject.parseObject(body); + Assert.assertEquals("INVALID JSON body", result.get("Error")); + Assert.assertNull(result.get(valueKey())); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/BaseHttpTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/BaseHttpTest.java index f1019abad55..e43b72b724e 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/BaseHttpTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/BaseHttpTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; import java.lang.reflect.Field; import javax.servlet.http.HttpServlet; @@ -20,6 +21,8 @@ import org.tron.core.Wallet; import org.tron.core.config.args.Args; import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.AccountContract.AccountCreateContract; /** * Base class for HTTP servlet unit tests. @@ -32,8 +35,19 @@ */ public abstract class BaseHttpTest { + /** + * A stand-in transaction for the mocked wallet. The type is the one an all-default Contract + * already declared (enum 0); what it did not carry was a matching payload, and + * Util.printTransactionToJSON drops a contract whose Any does not match its declared type -- + * silently, and only once TransactionFactory knows that type, which depends on whether any + * actuator has been constructed elsewhere in the jvm. Packing the payload the way the node's + * own builders do makes the rendering independent of that global state. + */ protected static final Transaction MINIMAL_TX = Transaction.newBuilder() - .setRawData(Transaction.raw.newBuilder().addContract(Transaction.Contract.newBuilder())) + .setRawData(Transaction.raw.newBuilder().addContract( + Transaction.Contract.newBuilder() + .setType(ContractType.AccountCreateContract) + .setParameter(Any.pack(AccountCreateContract.getDefaultInstance())))) .build(); @Mock diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/BroadcastHexAddressValidationTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/BroadcastHexAddressValidationTest.java new file mode 100644 index 00000000000..e2d44728efe --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/servlets/BroadcastHexAddressValidationTest.java @@ -0,0 +1,144 @@ +package org.tron.core.services.http.servlets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.util.Arrays; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.GrpcAPI.Return.response_code; +import org.tron.common.utils.Base58; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.StringUtil; +import org.tron.core.Wallet; +import org.tron.json.JSONObject; +import org.tron.protos.Protocol; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.contract.BalanceContract.TransferContract; + +public class BroadcastHexAddressValidationTest { + + @Test + public void rendersOversizedAuthAddressAsHexWithoutBase58Conversion() throws Exception { + for (int size : new int[]{129, 4096, 16384, 65536}) { + Wallet wallet = mock(Wallet.class); + Transaction transaction = transaction(size); + when(wallet.broadcastTransaction(transaction)).thenReturn(Return.newBuilder() + .setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR).build()); + try (MockedStatic base58 = mockStatic(Base58.class)) { + JSONObject result = broadcast(transaction, wallet); + assertEquals(response_code.CONTRACT_VALIDATE_ERROR.name(), result.getString("code")); + assertTrue(result.getString("transaction") + .contains(ByteArray.toHexString(bytes(size).toByteArray()))); + base58.verifyNoInteractions(); + } + } + } + + @Test + public void preservesSuccessfulAndRejectedBroadcastResponses() throws Exception { + Transaction transaction = transaction(21); + for (response_code code : new response_code[]{response_code.SUCCESS, response_code.SIGERROR}) { + Wallet wallet = mock(Wallet.class); + boolean success = code == response_code.SUCCESS; + Return result = Return.newBuilder().setResult(success).setCode(code) + .setMessage(ByteString.copyFromUtf8("test result")).build(); + when(wallet.broadcastTransaction(transaction)).thenReturn(result); + JSONObject response = broadcast(transaction, wallet); + assertEquals(success, response.getBoolean("result")); + assertEquals(code.name(), response.getString("code")); + assertEquals("test result", response.getString("message")); + assertEquals(JsonFormat.printToString(transaction, true), response.getString("transaction")); + assertEquals(64, response.getString("txid").length()); + verify(wallet).broadcastTransaction(transaction); + } + } + + @Test + public void jsonFormatterPreservesTwentyByteLogAddresses() { + ByteString address = bytes(20); + Protocol.TransactionInfo.Log log = Protocol.TransactionInfo.Log.newBuilder() + .setAddress(address).build(); + JSONObject json = JSONObject.parseObject(JsonFormat.printToString(log, true)); + assertEquals(StringUtil.encode58Check(address.toByteArray()), json.getString("address")); + assertEquals(StringUtil.encode58Check(bytes(128).toByteArray()), JSONObject.parseObject( + JsonFormat.printToString(log.toBuilder().setAddress(bytes(128)).build(), true)) + .getString("address")); + assertEquals(ByteArray.toHexString(bytes(129).toByteArray()), JSONObject.parseObject( + JsonFormat.printToString(log.toBuilder().setAddress(bytes(129)).build(), true)) + .getString("address")); + assertEquals(ByteArray.toHexString(bytes(129).toByteArray()), JSONObject.parseObject( + JsonFormat.printToString(log.toBuilder().setAddress(bytes(129)).build(), false)) + .getString("address")); + } + + @Test + public void preservesGenesisAddressRendering() { + ByteString witness = ByteString.copyFromUtf8("A new system must allow existing systems to be " + + "linked together without requiring any central control or coordination"); + JSONObject header = JSONObject.parseObject(JsonFormat.printToString( + Protocol.BlockHeader.raw.newBuilder().setWitnessAddress(witness).build(), true)); + assertEquals("A2aRHeJL17i55Nd5FWdtaXJL4RuhcBPnZGSqvDZnw2Qcadhh9hhFBwAkHM52Tuv6d15wMcMgrUc3hi1Hp" + + "CCyAefw5kDgbvbGWjKgJoyER8ZY1w6sy3pxp56YdtiW4LtbCKaWyRpxzXv4mEmh88Y2ipZP8VFpSy8YLqnj", + header.getString("witness_address")); + + ByteString owner = ByteString.copyFromUtf8("0x000000000000000000000"); + JSONObject transfer = JSONObject.parseObject(JsonFormat.printToString( + TransferContract.newBuilder().setOwnerAddress(owner).build(), true)); + assertEquals("7YxAaK71utTpYJ8u4Zna7muWxd1pQwimpGxy8", transfer.getString("owner_address")); + } + + @Test + public void rawAnyPayloadIsStillHexRatherThanAnExpandedAddress() { + Transaction transaction = Transaction.newBuilder().setRawData(Transaction.raw.newBuilder() + .addContract(Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder() + .setOwnerAddress(bytes(4096)).build())))).build(); + assertEquals(JsonFormat.printToString(transaction, false), + JsonFormat.printToString(transaction, true)); + } + + private static JSONObject broadcast(Transaction transaction, Wallet wallet) throws Exception { + BroadcastHexServlet servlet = new BroadcastHexServlet(); + Field walletField = BroadcastHexServlet.class.getDeclaredField("wallet"); + walletField.setAccessible(true); + walletField.set(servlet, wallet); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + JSONObject body = new JSONObject(); + body.put("transaction", ByteArray.toHexString(transaction.toByteArray())); + when(request.getReader()).thenReturn(new BufferedReader(new StringReader(body.toJSONString()))); + StringWriter output = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(output)); + servlet.doPost(request, response); + return JSONObject.parseObject(output.toString()); + } + + private static Transaction transaction(int size) { + return Transaction.newBuilder().setRawData(Transaction.raw.newBuilder() + .addAuths(Protocol.authority.newBuilder().setAccount(Protocol.AccountId.newBuilder() + .setAddress(bytes(size))))) + .addSignature(ByteString.copyFrom(new byte[]{1})).build(); + } + + private static ByteString bytes(int size) { + byte[] input = new byte[size]; + Arrays.fill(input, (byte) 0x41); + return ByteString.copyFrom(input); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/GetBrokerageServletTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/GetBrokerageServletTest.java index 5a778d077a2..64286fc6941 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/GetBrokerageServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/GetBrokerageServletTest.java @@ -1,20 +1,18 @@ package org.tron.core.services.http.servlets; -import java.io.UnsupportedEncodingException; import javax.annotation.Resource; import org.junit.Assert; 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; import org.tron.json.JSONObject; -public class GetBrokerageServletTest extends BaseTest { +public class GetBrokerageServletTest extends AddressQueryServletTestBase { @Resource - private GetBrokerageServlet getBrokerageServlet; + private GetBrokerageServlet getBrokerageServlet; static { Args.setParam( @@ -24,84 +22,40 @@ public class GetBrokerageServletTest extends BaseTest { ); } - public MockHttpServletRequest createRequest(String contentType) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setMethod("POST"); - request.setContentType(contentType); - request.setCharacterEncoding("UTF-8"); - return request; + @Override + protected void invokeGet(MockHttpServletRequest request, MockHttpServletResponse response) { + getBrokerageServlet.doGet(request, response); } - @Test - public void getBrokerageValueByJsonTest() { - int expect = 20; - String jsonParam = "{\"address\": \"TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W\"}"; - MockHttpServletRequest request = createRequest("application/json"); - request.setContent(jsonParam.getBytes()); - MockHttpServletResponse response = new MockHttpServletResponse(); + @Override + protected void invokePost(MockHttpServletRequest request, MockHttpServletResponse response) { getBrokerageServlet.doPost(request, response); - try { - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int brokerage = (int)result.get("brokerage"); - Assert.assertEquals(expect, brokerage); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } } + @Override + protected String valueKey() { + return "brokerage"; + } - @Test - public void getBrokerageByJsonUTF8Test() { - int expect = 20; - String jsonParam = "{\"address\": \"TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W\"}"; - MockHttpServletRequest request = createRequest("application/json; charset=utf-8"); - request.setContent(jsonParam.getBytes()); - MockHttpServletResponse response = new MockHttpServletResponse(); - getBrokerageServlet.doPost(request, response); - try { - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int brokerage = (int)result.get("brokerage"); - Assert.assertEquals(expect, brokerage); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } + /** An address the delegation store holds nothing for still answers the default brokerage. */ + @Override + protected int valueWithoutState() { + return 20; } @Test - public void getBrokerageValueTest() { - int expect = 20; - MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); - request.addParameter("address", "TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W"); - MockHttpServletResponse response = new MockHttpServletResponse(); - getBrokerageServlet.doPost(request, response); - try { - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int brokerage = (int)result.get("brokerage"); - Assert.assertEquals(expect, brokerage); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } + public void getBrokerageValueByJsonTest() throws Exception { + MockHttpServletRequest request = jsonRequest("TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W"); + + JSONObject result = JSONObject.parseObject(invoke(request)); + Assert.assertEquals(20, (int) result.get("brokerage")); } @Test - public void getByBlankParamTest() { - int expect = 0; - MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); - request.addParameter("address", ""); - MockHttpServletResponse response = new MockHttpServletResponse(); - getBrokerageServlet.doPost(request, response); - try { - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int brokerage = (int)result.get("brokerage"); - Assert.assertEquals(expect, brokerage); - String content = (String) result.get("Error"); - Assert.assertNull(content); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } + public void getBrokerageValueTest() throws Exception { + MockHttpServletRequest request = formRequest("TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W"); + + JSONObject result = JSONObject.parseObject(invoke(request)); + Assert.assertEquals(20, (int) result.get("brokerage")); } } diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/GetExchangeByIdServletTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/GetExchangeByIdServletTest.java index e62cef02608..719a2c1dbaa 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/GetExchangeByIdServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/GetExchangeByIdServletTest.java @@ -1,10 +1,12 @@ package org.tron.core.services.http.servlets; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; @@ -49,4 +51,17 @@ public void testGet() throws Exception { assertEquals(200, response.getStatus()); assertTrue(response.getContentAsString().contains("exchange_id")); } + + @Test + public void oversizedQuotedIdIsRejectedBeforeWalletCall() throws Exception { + String oversized = "99999999999999999999999999999999999999999999999999999999999999999"; + MockHttpServletResponse response = newResponse(); + + servlet.doPost(postRequest("{\"id\":\"" + oversized + "\"}"), response); + + verifyNoInteractions(wallet); + assertTrue(response.getContentAsString().contains("id")); + assertTrue(response.getContentAsString().contains("64-bit integer")); + assertFalse(response.getContentAsString().contains(oversized)); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/GetRewardServletTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/GetRewardServletTest.java index 10920c4fb8b..6b20711f5ca 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/GetRewardServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/GetRewardServletTest.java @@ -2,7 +2,6 @@ import static org.tron.common.utils.Commons.decodeFromBase58Check; -import java.io.UnsupportedEncodingException; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -10,7 +9,6 @@ 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; import org.tron.core.db.Manager; @@ -19,7 +17,7 @@ import org.tron.json.JSONObject; @Slf4j -public class GetRewardServletTest extends BaseTest { +public class GetRewardServletTest extends AddressQueryServletTestBase { @Resource private Manager manager; @@ -31,22 +29,30 @@ public class GetRewardServletTest extends BaseTest { private DelegationStore delegationStore; @Resource - GetRewardServlet getRewardServlet; + private GetRewardServlet getRewardServlet; static { - Args.setParam( - new String[]{ - "--output-directory", dbPath(), - }, TestConstants.TEST_CONF - ); + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); } - public MockHttpServletRequest createRequest(String contentType) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setMethod("POST"); - request.setContentType(contentType); - request.setCharacterEncoding("UTF-8"); - return request; + @Override + protected void invokeGet(MockHttpServletRequest request, MockHttpServletResponse response) { + getRewardServlet.doGet(request, response); + } + + @Override + protected void invokePost(MockHttpServletRequest request, MockHttpServletResponse response) { + getRewardServlet.doPost(request, response); + } + + @Override + protected String valueKey() { + return "reward"; + } + + @Override + protected int valueWithoutState() { + return 0; } @Before @@ -58,107 +64,19 @@ public void init() { } @Test - public void getRewardValueByJsonTest() { - int expect = 138181; - String jsonParam = "{\"address\": \"TNboetpFgv9SqMoHvaVt626NLXETnbdW1K\"}"; - MockHttpServletRequest request = createRequest("application/json"); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.setContent(jsonParam.getBytes()); - try { - getRewardServlet.doPost(request, response); - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int reward = (int)result.get("reward"); - Assert.assertEquals(expect, reward); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } - } + public void getRewardValueByJsonTest() throws Exception { + MockHttpServletRequest request = jsonRequest("TNboetpFgv9SqMoHvaVt626NLXETnbdW1K"); - @Test - public void getRewardByJsonUTF8Test() { - int expect = 138181; - String jsonParam = "{\"address\": \"TNboetpFgv9SqMoHvaVt626NLXETnbdW1K\"}"; - MockHttpServletRequest request = createRequest("application/json; charset=utf-8"); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.setContent(jsonParam.getBytes()); - try { - getRewardServlet.doPost(request, response); - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int reward = (int)result.get("reward"); - Assert.assertEquals(expect, reward); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } + JSONObject result = JSONObject.parseObject(invoke(request)); + Assert.assertEquals(138181, (int) result.get("reward")); } @Test - public void getRewardValueTest() { - int expect = 138181; - MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); - MockHttpServletResponse response = new MockHttpServletResponse(); + public void getRewardValueTest() throws Exception { mortgageService.payStandbyWitness(); - request.addParameter("address", "TNboetpFgv9SqMoHvaVt626NLXETnbdW1K"); - getRewardServlet.doPost(request, response); - try { - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int reward = (int)result.get("reward"); - Assert.assertEquals(expect, reward); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } - } - - @Test - public void getByBlankParamTest() { - MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.addParameter("address", ""); - GetRewardServlet getRewardServlet = new GetRewardServlet(); - getRewardServlet.doPost(request, response); - try { - String contentAsString = response.getContentAsString(); - JSONObject result = JSONObject.parseObject(contentAsString); - int reward = (int)result.get("reward"); - Assert.assertEquals(0, reward); - String content = (String) result.get("Error"); - Assert.assertNull(content); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } - } + MockHttpServletRequest request = formRequest("TNboetpFgv9SqMoHvaVt626NLXETnbdW1K"); - @Test - public void getRewardByOversizedValidCharAddressTest() { - // 41-char, all-valid-Base58 address: the length guard returns null -> reward 0. - MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.addParameter("address", "T" + new String(new char[40]).replace('\0', 'a')); - new GetRewardServlet().doPost(request, response); - try { - JSONObject result = JSONObject.parseObject(response.getContentAsString()); - Assert.assertEquals(0, (int) result.get("reward")); - Assert.assertNull(result.get("Error")); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } + JSONObject result = JSONObject.parseObject(invoke(request)); + Assert.assertEquals(138181, (int) result.get("reward")); } - - @Test - public void getRewardByOversizedIllegalCharAddressTest() { - MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.addParameter("address", "T" + new String(new char[40]).replace('\0', '0')); - new GetRewardServlet().doPost(request, response); - try { - JSONObject result = JSONObject.parseObject(response.getContentAsString()); - Assert.assertEquals(0, (int) result.get("reward")); - Assert.assertNull(result.get("Error")); - } catch (UnsupportedEncodingException e) { - Assert.fail(e.getMessage()); - } - } - } diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/JsonLongValueValidationTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/JsonLongValueValidationTest.java new file mode 100644 index 00000000000..c6a0cda084f --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/servlets/JsonLongValueValidationTest.java @@ -0,0 +1,107 @@ +package org.tron.core.services.http.servlets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +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.security.InvalidParameterException; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.tron.core.exception.InvalidHttpParameterException; +import org.tron.json.JSONObject; + +public class JsonLongValueValidationTest { + + private static JSONObject value(String value) { + return JSONObject.parseObject("{\"id\":\"" + value + "\"}"); + } + + /** The same value submitted unquoted, which reaches the node as a number rather than a string. */ + private static JSONObject unquoted(String value) { + return JSONObject.parseObject("{\"id\":" + value + "}"); + } + + @Test + public void stringLengthBoundaryIsEnforcedBeforeDecimalConversion() { + assertEquals(0L, Util.getJsonLongValue(value(StringUtils.repeat('0', 64)), "id", true)); + + String oversized = StringUtils.repeat('9', 65); + InvalidParameterException exception = assertThrows(InvalidParameterException.class, + () -> Util.getJsonLongValue(value(oversized), "id", true)); + assertTrue(exception.getMessage().contains("id")); + assertFalse(exception.getMessage().contains(oversized)); + } + + /** An unquoted token reaches this check as an already-normalized Number. */ + @Test + public void unquotedNumberNormalizedTextLengthBoundaryIsEnforced() { + // no 64-character normalized number fits in a long, so the accepted side of the bound is + // shown by where the rejection comes from: past the bound, the exact conversion refuses it + InvalidHttpParameterException converted = assertThrows(InvalidHttpParameterException.class, + () -> Util.getJsonLongValue(unquoted(StringUtils.repeat('9', 64)), "id", true)); + assertTrue(converted.getCause() instanceof ArithmeticException); + + String oversized = StringUtils.repeat('9', 65); + InvalidHttpParameterException exception = assertThrows(InvalidHttpParameterException.class, + () -> Util.getJsonLongValue(unquoted(oversized), "id", true)); + assertNull(exception.getCause()); + assertTrue(exception.getMessage().contains("id")); + assertFalse(exception.getMessage().contains(oversized)); + } + + @Test + public void exactLongRepresentationsRemainCompatible() { + assertEquals(Long.MIN_VALUE, + Util.getJsonLongValue(value(Long.toString(Long.MIN_VALUE)), "id", true)); + assertEquals(Long.MAX_VALUE, + Util.getJsonLongValue(value(Long.toString(Long.MAX_VALUE)), "id", true)); + assertEquals(0L, Util.getJsonLongValue(value("0"), "id", true)); + assertEquals(1L, Util.getJsonLongValue(value("1.0"), "id", true)); + assertEquals(100L, Util.getJsonLongValue(value("1e2"), "id", true)); + } + + @Test + public void conversionFailuresAreReportedAsInvalidParameter() { + String[] rawJsonValues = {"\"1.5\"", "1.5", "\"9223372036854775808\"", "\"abc\""}; + for (String rawJsonValue : rawJsonValues) { + JSONObject jsonObject = JSONObject.parseObject("{\"id\":" + rawJsonValue + "}"); + InvalidHttpParameterException exception = assertThrows( + InvalidHttpParameterException.class, + () -> Util.getJsonLongValue(jsonObject, "id", true)); + assertEquals("invalid key [id]: expect a 64-bit integer", exception.getMessage()); + assertNotNull(exception.getCause()); + } + } + + /** + * A container is not a number. It used to skip the length bound altogether and reach the + * conversion as its full serialized form, bounded only by the shim's 65535-character fallback. + */ + @Test + public void nonNumericTypesAreRejectedBeforeConversion() { + String digits = StringUtils.repeat('9', 128); + String[] rawJsonValues = {"[" + digits + "]", "{\"a\":" + digits + "}", "true"}; + for (String rawJsonValue : rawJsonValues) { + JSONObject jsonObject = JSONObject.parseObject("{\"id\":" + rawJsonValue + "}"); + InvalidParameterException exception = assertThrows(InvalidParameterException.class, + () -> Util.getJsonLongValue(jsonObject, "id", true)); + assertTrue(exception.getMessage().contains("id")); + assertFalse(exception.getMessage().contains(digits)); + } + } + + /** An absent optional key must stay distinguishable from one the node refuses. */ + @Test + public void optionalMissingValueStillDefaultsToZero() { + assertEquals(0L, Util.getJsonLongValue(new JSONObject(), "id", false)); + } + + @Test + public void requiredMissingValueStillFails() { + assertThrows(InvalidParameterException.class, + () -> Util.getJsonLongValue(new JSONObject(), "id", true)); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/TransferServletTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/TransferServletTest.java index 96875375fb8..e8c99a96028 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/TransferServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/TransferServletTest.java @@ -1,5 +1,8 @@ package org.tron.core.services.http.servlets; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -12,6 +15,7 @@ import org.tron.common.crypto.ECKey; import org.tron.common.utils.ByteArray; import org.tron.core.capsule.TransactionCapsule; +import org.tron.json.JSONObject; import org.tron.protos.Protocol; import org.tron.protos.contract.BalanceContract; @@ -52,4 +56,90 @@ && addressEquals(((BalanceContract.TransferContract) c) eq(Protocol.Transaction.Contract.ContractType.TransferContract)); assertTransactionResponse(response); } + + private String transferJson(String permissionIdJson) { + return "{" + + "\"owner_address\": \"" + ownerAddr + "\"," + + "\"to_address\": \"" + toAddr + "\"," + + "\"amount\": 100," + + "\"Permission_id\": " + permissionIdJson + + "}"; + } + + private MockHttpServletResponse post(String permissionIdJson) throws Exception { + MockHttpServletResponse response = newResponse(); + servlet.doPost(postRequest(transferJson(permissionIdJson)), response); + return response; + } + + @Test + public void testPermissionIdReachesTheBuiltTransaction() throws Exception { + MockHttpServletResponse response = post("2"); + + assertTransactionResponse(response); + JSONObject contract = JSONObject.parseObject(response.getContentAsString()) + .getJSONObject("raw_data").getJSONArray("contract").getJSONObject(0); + assertEquals(2, contract.getIntValue("Permission_id")); + } + + /** + * A fraction never reaches Util.setTransactionPermissionId. Permission_id is not a + * TransferContract field, so JsonFormat.merge skips it through handleMissingField, which + * accepts only integers, booleans, strings and null -- lookingAtInteger() sees the leading + * digit, hands "1.9" to consumeInt64() and that fails first. Kept as a guard that the + * endpoint rejects it whichever layer does the rejecting. + */ + @Test + public void testFractionalPermissionIdIsRejected() throws Exception { + String content = post("1.9").getContentAsString(); + + assertTrue("must report an error", content.contains("\"Error\"")); + assertFalse("must not hand back a transaction", content.contains("txID")); + assertFalse(content.contains("\"raw_data\"")); + } + + /** + * The reachable case: 2^32+1 is a valid int64, so it clears JsonFormat.merge and lands in + * Util.setTransactionPermissionId, where BigDecimal.intValue() used to wrap it to 1 -- + * handing back a transaction built against permission 1, which the caller never asked for. + */ + @Test + public void testOutOfRangePermissionIdIsRejected() throws Exception { + String content = post("4294967297").getContentAsString(); + + assertTrue("must report an error", content.contains("\"Error\"")); + assertFalse("must not hand back a transaction built against permission 1", + content.contains("txID")); + assertFalse("must not echo the submitted value", content.contains("4294967297")); + } + + /** + * An explicit null used to reach getInteger(), whose null return blew up on unboxing with a + * bare NullPointerException. It stays an error -- writing the key states intent, so a null + * there is a caller-side defect. Omitting the key is how a caller asks for the default. + */ + @Test + public void testExplicitNullPermissionIdIsRejected() throws Exception { + String content = post("null").getContentAsString(); + + assertTrue("must report an error", content.contains("\"Error\"")); + assertFalse("must not hand back a transaction", content.contains("txID")); + assertFalse("must not surface a NullPointerException", + content.contains("NullPointerException")); + } + + @Test + public void testOmittedPermissionIdBuildsTransactionWithoutPermission() throws Exception { + MockHttpServletResponse response = newResponse(); + servlet.doPost(postRequest("{" + + "\"owner_address\": \"" + ownerAddr + "\"," + + "\"to_address\": \"" + toAddr + "\"," + + "\"amount\": 100" + + "}"), response); + + assertTransactionResponse(response); + JSONObject contract = JSONObject.parseObject(response.getContentAsString()) + .getJSONObject("raw_data").getJSONArray("contract").getJSONObject(0); + assertFalse(contract.containsKey("Permission_id")); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/UtilProcessErrorTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/UtilProcessErrorTest.java index 23480c5c4c6..a978fa466c9 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/UtilProcessErrorTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/UtilProcessErrorTest.java @@ -4,12 +4,14 @@ import static org.junit.Assert.assertThrows; import com.google.protobuf.InvalidProtocolBufferException; +import java.security.InvalidParameterException; import org.bouncycastle.util.encoders.DecoderException; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; import org.springframework.mock.web.MockHttpServletResponse; import org.tron.core.exception.ContractValidateException; import org.tron.core.exception.HeaderNotFound; +import org.tron.core.exception.InvalidHttpParameterException; import org.tron.core.exception.MaintenanceUnavailableException; import org.tron.core.exception.ZkProofValidateException; import org.tron.json.JSONException; @@ -28,6 +30,8 @@ public void exactCompatibilityTypesPreserveNonBlankMessage() throws Exception { "balance is not sufficient"); assertError(new MaintenanceUnavailableException("maintenance in progress"), "maintenance in progress"); + assertError(new InvalidHttpParameterException("invalid key [id]: expect a 64-bit integer"), + "invalid key [id]: expect a 64-bit integer"); } @Test @@ -40,6 +44,7 @@ public void unclassifiedTypesFailClosed() throws Exception { decoder, new HeaderNotFound("latest block not found"), new IllegalArgumentException("No enum constant internal.Type.VALUE"), + new InvalidParameterException("base parameter detail"), new IllegalAccessException(RATE_LIMITER_ERROR_MSG), new IllegalAccessException("other access failure"), new ZkProofValidateException("wrapped validation detail", true) @@ -65,6 +70,8 @@ public void nullBlankAndSubclassMessagesFailClosed() throws Exception { assertError(new JsonFormat.ParseException(""), INTERNAL_SERVER_ERROR); assertError(new JsonFormat.ParseException(" "), INTERNAL_SERVER_ERROR); assertError(new ContractValidateException("subclass message") { }, INTERNAL_SERVER_ERROR); + assertError(new InvalidHttpParameterException(null), INTERNAL_SERVER_ERROR); + assertError(new InvalidHttpParameterException(" "), INTERNAL_SERVER_ERROR); } @Test diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/UtilTest.java index 715c66ba888..3ad725f83b8 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/UtilTest.java @@ -1,7 +1,9 @@ package org.tron.core.services.http.servlets; import com.google.protobuf.ByteString; +import java.security.InvalidParameterException; import javax.annotation.Resource; +import org.apache.commons.lang3.StringUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -236,7 +238,7 @@ public void testPackCreateSmartContractOmitsNullAbiOutputs() throws Exception { Assert.assertEquals(0, contract.getNewContract().getAbi().getEntrys(0).getOutputsCount()); } - private Transaction buildTooManySigsTransaction() { + private Transaction buildTransferTransaction() { String strTransaction = "{\n" + " \"visible\": false,\n" + " \"txID\": \"fc33817936b06e50d4b6f1797e62f52d69af6c0da580a607241a9c03a48e390e\",\n" @@ -264,7 +266,11 @@ private Transaction buildTooManySigsTransaction() { + "0a1541c076305e35aea1fe45a772fcaaab8a36e87bdb551215415624c12e308b03a1a6b21d9b86e3942fac1a" + "b92b180a70b2ccb8ea8930\"\n" + "}"; - Transaction transaction = Util.packTransaction(strTransaction, false); + return Util.packTransaction(strTransaction, false); + } + + private Transaction buildTooManySigsTransaction() { + Transaction transaction = buildTransferTransaction(); int totalSignNum = dbManager.getDynamicPropertiesStore().getTotalSignNum(); ByteString dummySig = ByteString.copyFrom(new byte[65]); Transaction.Builder builder = transaction.toBuilder(); @@ -301,4 +307,89 @@ public void testPrintSignWeightTooManySigsHttpPath() { Assert.assertTrue(jsonObject.getJSONObject("result").getString("message") .contains("too many signatures")); } + + @Test + public void testPermissionIdRejectsNonNumericTypes() { + assertRejected("[" + StringUtils.repeat('9', 128) + "]"); + assertRejected("{\"a\":" + StringUtils.repeat('9', 128) + "}"); + assertRejected("true"); + } + + private Transaction applyPermissionId(String rawJsonValue) { + JSONObject jsonObject = + JSONObject.parseObject("{\"" + Util.PERMISSION_ID + "\":" + rawJsonValue + "}"); + return Util.setTransactionPermissionId(jsonObject, buildTransferTransaction()); + } + + private int permissionIdOf(Transaction transaction) { + return transaction.getRawData().getContract(0).getPermissionId(); + } + + @Test + public void testPermissionIdAcceptsExactIntegerRepresentations() { + Assert.assertEquals(2, permissionIdOf(applyPermissionId("2"))); + Assert.assertEquals(2, permissionIdOf(applyPermissionId("\"2\""))); + } + + private void assertRejected(String rawJsonValue) { + InvalidParameterException e = Assert.assertThrows(InvalidParameterException.class, + () -> applyPermissionId(rawJsonValue)); + Assert.assertTrue(e.getMessage().contains(Util.PERMISSION_ID)); + Assert.assertFalse("the message must not echo the submitted value", + e.getMessage().contains(rawJsonValue)); + } + + @Test + public void testPermissionIdRejectsNonIntegerNumbers() { + assertRejected("1.9"); + assertRejected("2.999"); + assertRejected("1.0"); + assertRejected("1e2"); + assertRejected("\"1e2\""); + assertRejected("\"1.9\""); + assertRejected("\"2.5\""); + assertRejected("\"1.90\""); + assertRejected("\"1.0\""); + assertRejected("\"1.\""); + assertRejected("\"1,000\""); + } + + @Test + public void testPermissionIdRejectsIntOverflow() { + assertRejected("4294967297"); + assertRejected("99999999999"); + assertRejected("\"4294967297\""); + assertRejected("\"2147483648\""); + assertRejected("\"-2147483649\""); + assertRejected("9" + StringUtils.repeat('9', 64)); + assertRejected("\"" + StringUtils.repeat('9', 10_000) + "\""); + } + + @Test + public void testPermissionIdRejectsNonNumber() { + assertRejected("\"abc\""); + assertRejected("true"); + assertRejected("[1]"); + } + + @Test + public void testPermissionIdRejectsExplicitNull() { + InvalidParameterException e = Assert.assertThrows(InvalidParameterException.class, + () -> applyPermissionId("null")); + Assert.assertTrue(e.getMessage().contains(Util.PERMISSION_ID)); + } + + @Test + public void testAbsentPermissionIdLeavesTransactionUnchanged() { + JSONObject jsonObject = JSONObject.parseObject("{\"amount\":1}"); + Transaction transaction = buildTransferTransaction(); + Assert.assertEquals(0, + permissionIdOf(Util.setTransactionPermissionId(jsonObject, transaction))); + } + + @Test + public void testPermissionIdNotPositiveLeavesTransactionUnchanged() { + Assert.assertEquals(0, permissionIdOf(applyPermissionId("0"))); + Assert.assertEquals(0, permissionIdOf(applyPermissionId("-1"))); + } } diff --git a/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java b/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java new file mode 100644 index 00000000000..83c82a4bda8 --- /dev/null +++ b/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java @@ -0,0 +1,137 @@ +package org.tron.core.zen; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mockStatic; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.api.GrpcAPI.BytesMessage; +import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; +import org.tron.common.utils.ByteUtil; +import org.tron.core.utils.ZenChainParams; +import org.tron.protos.contract.ShieldContract; + +public class ShieldedTRC20ParametersBuilderTest { + + /** SHA-256 of {@link #encode} per spends-receives-withAsk key; each output is several KB. */ + private static final String[][] BASE_DIGESTS = { + {"1-1-true", + "da4c52cbf2903d98f9eba9c2b537328448d767bee8c495d3c22087e272976238"}, + {"1-1-false", + "b8fa61b5b5230092591d45fa61c65e5bfea2995441cdb373547e378c7670f8df"}, + {"1-2-true", + "77a62c96cf7f03a0ff092661861a53596b9ef57b9f397efcc661482b0ecc1d6b"}, + {"1-2-false", + "b998f3d1fc5e309560476b25e38fdb1f4cb2ec9223ada4734f0c086f0013c29d"}, + {"2-1-true", + "7fb3c1231f29a606aa2509d3d6bc9a0c58d851a8710c21f453d556edcdf41675"}, + {"2-1-false", + "25d93b0be6e8e387d86932b2a2b87f39bea516b543c646d0ce55023250b591ce"}, + {"2-2-true", + "2d4525842ecef92b52c0b3a0dc8689d461e1be08788f29d03a150639f05fe9c6"}, + {"2-2-false", + "aa8cd6ebd1208a53e642d9aa796967d5ebe66a7aca7615965dccd9d727f7b624"}, + }; + + private static ByteString fill(int length, int seed) { + byte[] value = new byte[length]; + for (int i = 0; i < length; i++) { + value[i] = (byte) (seed * 31 + i); + } + return ByteString.copyFrom(value); + } + + /** Every field has its protocol size and its own content, so a misplaced field shows. */ + static ShieldedTRC20Parameters populated(int spends, int receives) { + ShieldedTRC20Parameters.Builder parameters = ShieldedTRC20Parameters.newBuilder(); + for (int i = 0; i < spends; i++) { + int seed = 16 * i; + parameters.addSpendDescription(ShieldContract.SpendDescription.newBuilder() + .setNullifier(fill(32, seed + 1)) + .setAnchor(fill(32, seed + 2)) + .setValueCommitment(fill(32, seed + 3)) + .setRk(fill(32, seed + 4)) + .setZkproof(fill(192, seed + 5)) + .setSpendAuthoritySignature(fill(64, seed + 6))); + } + for (int i = 0; i < receives; i++) { + int seed = 16 * i; + parameters.addReceiveDescription(ShieldContract.ReceiveDescription.newBuilder() + .setNoteCommitment(fill(32, seed + 7)) + .setValueCommitment(fill(32, seed + 8)) + .setEpk(fill(32, seed + 9)) + .setZkproof(fill(192, seed + 10)) + .setCEnc(fill(ZenChainParams.ZC_ENCCIPHERTEXT_SIZE, seed + 11)) + .setCOut(fill(ZenChainParams.ZC_OUTCIPHERTEXT_SIZE, seed + 12))); + } + return parameters.setBindingSignature(fill(64, 13)).build(); + } + + /** Differs from the signatures inside the descriptions, so the two paths give distinct bytes. */ + static List requestSignatures(int count) { + List signatures = new ArrayList<>(); + for (int i = 0; i < count; i++) { + signatures.add(BytesMessage.newBuilder().setValue(fill(64, 16 * i + 14)).build()); + } + return signatures; + } + + static String encode(int spends, int receives, boolean withAsk) throws Exception { + return new ShieldedTRC20ParametersBuilder("transfer").getTriggerContractInput( + populated(spends, receives), requestSignatures(spends), BigInteger.ZERO, withAsk, + new byte[21]); + } + + static String digest(String triggerInput) throws Exception { + return Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(Hex.decode(triggerInput))); + } + + private static String runEmpty(int spends, int receives) throws Exception { + ShieldedTRC20Parameters.Builder parameters = ShieldedTRC20Parameters.newBuilder(); + for (int i = 0; i < spends; i++) { + parameters.addSpendDescription(ShieldContract.SpendDescription.getDefaultInstance()); + } + for (int i = 0; i < receives; i++) { + parameters.addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()); + } + return new ShieldedTRC20ParametersBuilder("transfer").getTriggerContractInput( + parameters.build(), Collections.emptyList(), BigInteger.ZERO, true, new byte[21]); + } + + @Test + public void invalidCountsAreRejectedBeforeAnyMerge() throws Exception { + Object[][] invalidCounts = { + {0, 1, "invalid transfer input number"}, + {3, 1, "invalid transfer input number"}, + {1, 0, "invalid transfer output number"}, + {1, 3, "invalid transfer output number"}, + {1, 10_000, "invalid transfer output number"}, + }; + try (MockedStatic byteUtil = mockStatic(ByteUtil.class)) { + for (Object[] counts : invalidCounts) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> runEmpty((int) counts[0], (int) counts[1])); + assertEquals(counts[2], exception.getMessage()); + } + byteUtil.verifyNoInteractions(); + } + } + + @Test + public void populatedOutputsMatchTheBaseForEveryCountAndSignaturePath() throws Exception { + for (String[] vector : BASE_DIGESTS) { + String[] key = vector[0].split("-"); + String triggerInput = encode(Integer.parseInt(key[0]), Integer.parseInt(key[1]), + Boolean.parseBoolean(key[2])); + assertEquals(vector[0], vector[1], digest(triggerInput)); + } + } +}