diff --git a/.ci/ci_check_commit.sh b/.ci/ci_check_commit.sh index 15ae89e5c..3c02ba469 100755 --- a/.ci/ci_check_commit.sh +++ b/.ci/ci_check_commit.sh @@ -3,7 +3,7 @@ set -e scan_code_script="cobra/cobra.py -f json -o /tmp/report.json -t " -ignore_files=(ECCParams.java ECKeyPair.java KeyUtils.java Permission.java Frozen.java ECDSASign.java Constant.java PerformanceOkDSync.java SM2Algorithm.java SM2KeyGenerator.java test) +ignore_files=(RevertResolver.java ECCParams.java ECKeyPair.java KeyUtils.java Permission.java Frozen.java ECDSASign.java Constant.java PerformanceOkDSync.java SM2Algorithm.java SM2KeyGenerator.java test) LOG_ERROR() { content=${1} diff --git a/build.gradle b/build.gradle index 9d8f0fb67..a73fd384c 100644 --- a/build.gradle +++ b/build.gradle @@ -150,6 +150,10 @@ try { println(" .git directory not exist."); } +tasks.withType(Test) { + maxParallelForks = 1 +} + // 1 dist jar jar { destinationDir file('dist/apps') diff --git a/src/main/java/org/fisco/bcos/channel/client/Service.java b/src/main/java/org/fisco/bcos/channel/client/Service.java index e4a957b5c..e5abd890f 100644 --- a/src/main/java/org/fisco/bcos/channel/client/Service.java +++ b/src/main/java/org/fisco/bcos/channel/client/Service.java @@ -71,6 +71,8 @@ import org.fisco.bcos.web3j.protocol.core.methods.response.Log; import org.fisco.bcos.web3j.protocol.core.methods.response.TransactionReceipt; import org.fisco.bcos.web3j.protocol.exceptions.TransactionException; +import org.fisco.bcos.web3j.tuples.generated.Tuple2; +import org.fisco.bcos.web3j.tx.RevertResolver; import org.fisco.bcos.web3j.tx.txdecode.LogResult; import org.fisco.bcos.web3j.utils.Strings; import org.slf4j.Logger; @@ -716,13 +718,6 @@ public void run(Timeout timeout) throws Exception { } } - /** - * When SDK start, the initial subscribed topics information set by user will be sent to the - * node. User can update subscribed topics again by following steps: 1. Set the topics you want - * to subscribe to again Service service // Servcie object Set topics // topics that - * subscribe again service.setTopics(topics) 2. send update topics message to all nodes - * service.updateTopicsToNode(); - */ public void updateTopicsToNode() { logger.info(" updateTopicToNode, groupId: {}, topics: {}", groupId, getTopics()); @@ -1548,6 +1543,12 @@ public void onReceiveTransactionMessage(String seq, TransactionReceipt receipt) } try { + Tuple2 revertMessage = + RevertResolver.tryResolveRevertMessage(receipt); + if (revertMessage.getValue1()) { + logger.debug(" revert message: {}", revertMessage.getValue2()); + receipt.setMessage(revertMessage.getValue2()); + } callback.onResponse(receipt); } catch (Exception e) { logger.error("Error process transactionMessage: ", e); diff --git a/src/main/java/org/fisco/bcos/web3j/protocol/channel/ChannelEthereumService.java b/src/main/java/org/fisco/bcos/web3j/protocol/channel/ChannelEthereumService.java index 2ba9b2769..7c3d7ff55 100644 --- a/src/main/java/org/fisco/bcos/web3j/protocol/channel/ChannelEthereumService.java +++ b/src/main/java/org/fisco/bcos/web3j/protocol/channel/ChannelEthereumService.java @@ -11,6 +11,8 @@ import org.fisco.bcos.web3j.protocol.core.methods.response.SendTransaction; import org.fisco.bcos.web3j.protocol.core.methods.response.TransactionReceipt; import org.fisco.bcos.web3j.protocol.exceptions.MessageDecodingException; +import org.fisco.bcos.web3j.tuples.generated.Tuple2; +import org.fisco.bcos.web3j.tx.RevertResolver; import org.fisco.bcos.web3j.tx.exceptions.ContractCallException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,17 +82,26 @@ public T send(Request request, Class responseType) throw } if (t.getResult() instanceof CallOutput) { CallOutput callResult = (CallOutput) t.getResult(); - if (StatusCode.RevertInstruction.equals(callResult.getStatus())) { - throw new ContractCallException( - "The execution of the contract rolled back."); - } - if (StatusCode.CallAddressError.equals(callResult.getStatus())) { - throw new ContractCallException("The contract address is incorrect."); - } - if (!StatusCode.Success.equals(callResult.getStatus())) { - throw new ContractCallException( - StatusCode.getStatusMessage(callResult.getStatus())); + Tuple2 revertMessage = + RevertResolver.tryResolveRevertMessage( + callResult.getStatus(), callResult.getOutput()); + if (revertMessage.getValue1()) { + logger.debug(" revert message: {}", revertMessage.getValue2()); + throw new ContractCallException(revertMessage.getValue2()); + } else { + if (StatusCode.RevertInstruction.equals(callResult.getStatus())) { + throw new ContractCallException( + "The execution of the contract rolled back."); + } + if (StatusCode.CallAddressError.equals(callResult.getStatus())) { + throw new ContractCallException("The contract address is incorrect."); + } + + if (!StatusCode.Success.equals(callResult.getStatus())) { + throw new ContractCallException( + StatusCode.getStatusMessage(callResult.getStatus())); + } } } return t; diff --git a/src/main/java/org/fisco/bcos/web3j/protocol/core/Ethereum.java b/src/main/java/org/fisco/bcos/web3j/protocol/core/Ethereum.java index 85f12b0e1..57885e588 100644 --- a/src/main/java/org/fisco/bcos/web3j/protocol/core/Ethereum.java +++ b/src/main/java/org/fisco/bcos/web3j/protocol/core/Ethereum.java @@ -1,7 +1,9 @@ package org.fisco.bcos.web3j.protocol.core; +import java.io.IOException; import java.math.BigInteger; import java.util.List; +import org.fisco.bcos.channel.client.TransactionSucCallback; import org.fisco.bcos.web3j.protocol.core.methods.response.BcosBlock; import org.fisco.bcos.web3j.protocol.core.methods.response.BcosFilter; import org.fisco.bcos.web3j.protocol.core.methods.response.BcosLog; @@ -97,6 +99,9 @@ Request call( Request sendRawTransaction(String signedTransactionData); + void sendRawTransaction(String signedTransactionData, TransactionSucCallback callback) + throws IOException; + // generateGroup Request generateGroup(int groupId, int timestamp, List nodeList); diff --git a/src/main/java/org/fisco/bcos/web3j/protocol/core/JsonRpc2_0Web3j.java b/src/main/java/org/fisco/bcos/web3j/protocol/core/JsonRpc2_0Web3j.java index e9d609e5b..df3ebcf3a 100644 --- a/src/main/java/org/fisco/bcos/web3j/protocol/core/JsonRpc2_0Web3j.java +++ b/src/main/java/org/fisco/bcos/web3j/protocol/core/JsonRpc2_0Web3j.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; +import org.fisco.bcos.channel.client.TransactionSucCallback; import org.fisco.bcos.web3j.protocol.Web3j; import org.fisco.bcos.web3j.protocol.Web3jService; import org.fisco.bcos.web3j.protocol.channel.ChannelEthereumService; @@ -318,6 +319,16 @@ public Request sendRawTransaction(String signedTransactionDa SendTransaction.class); } + @Override + public void sendRawTransaction(String signedTransactionData, TransactionSucCallback callback) + throws IOException { + Request request = sendRawTransaction(signedTransactionData); + request.setNeedTransCallback(true); + request.setTransactionSucCallback(callback); + + request.sendOnly(); + } + @Override public Request getGroupPeers() { return new Request<>( diff --git a/src/main/java/org/fisco/bcos/web3j/tx/RevertResolver.java b/src/main/java/org/fisco/bcos/web3j/tx/RevertResolver.java new file mode 100644 index 000000000..117aa5db4 --- /dev/null +++ b/src/main/java/org/fisco/bcos/web3j/tx/RevertResolver.java @@ -0,0 +1,113 @@ +package org.fisco.bcos.web3j.tx; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.List; +import org.fisco.bcos.web3j.abi.FunctionReturnDecoder; +import org.fisco.bcos.web3j.abi.TypeReference; +import org.fisco.bcos.web3j.abi.datatypes.Function; +import org.fisco.bcos.web3j.abi.datatypes.Type; +import org.fisco.bcos.web3j.abi.datatypes.Utf8String; +import org.fisco.bcos.web3j.protocol.core.methods.response.TransactionReceipt; +import org.fisco.bcos.web3j.tuples.generated.Tuple2; +import org.fisco.bcos.web3j.utils.Numeric; +import org.fisco.bcos.web3j.utils.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* +pragma solidity ^0.4.25; +contract Revert { + function Error(string memory s) public {} +} + +"08c379a0": "Error(string)" // Not SM Method +"c703cb12": "Error(string)" // SM Method +*/ + +public class RevertResolver { + + private static final Logger logger = LoggerFactory.getLogger(RevertResolver.class); + + public static final String RevertMethod = "08c379a0"; + public static final String RevertMethodWithHexPrefix = "0x08c379a0"; + + public static final String SMRevertMethod = "c703cb12"; + public static final String SMRevertMethodWithHexPrefix = "0xc703cb12"; + + // Error(String) + public static final Function revertFunction = + new Function( + "Error", + Collections.emptyList(), + Collections.singletonList(new TypeReference() {})); + + /** + * Does output start with the code of the Revert method, If so, the output may be error message + * + * @param output + * @return + */ + public static boolean isOutputStartWithRevertMethod(String output) { + return output.startsWith(RevertMethodWithHexPrefix) + || output.startsWith(SMRevertMethodWithHexPrefix) + || (output.startsWith(RevertMethod) || output.startsWith(SMRevertMethod)); + } + + /** + * @param status + * @param output + * @return + */ + public static boolean hasRevertMessage(String status, String output) { + if (Strings.isEmpty(status) || Strings.isEmpty(output)) { + return false; + } + try { + BigInteger statusQuantity = Numeric.decodeQuantity(status); + return !BigInteger.ZERO.equals(statusQuantity) && isOutputStartWithRevertMethod(output); + } catch (Exception e) { + return false; + } + } + + /** + * @param status + * @param output + * @return + */ + public static Tuple2 tryResolveRevertMessage(String status, String output) { + if (!hasRevertMessage(status, output)) { + return new Tuple2<>(false, null); + } + + try { + // 00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030497373756572526f6c653a2063616c6c657220646f6573206e6f742068617665207468652049737375657220726f6c6500000000000000000000000000000000 + String rawOutput = + Numeric.containsHexPrefix(output) + ? output.substring(RevertMethodWithHexPrefix.length()) + : output.substring(RevertMethod.length()); + List result = + FunctionReturnDecoder.decode(rawOutput, revertFunction.getOutputParameters()); + if (result.get(0) instanceof Utf8String) { + String message = ((Utf8String) result.get(0)).getValue(); + if (logger.isDebugEnabled()) { + logger.debug(" ABI: {} , RevertMessage: {}", output, message); + } + return new Tuple2<>(true, message); + } + } catch (Exception e) { + logger.warn(" ABI: {}, e: {}", output, e); + } + + return new Tuple2<>(false, null); + } + + /** + * @param receipt + * @return + */ + public static Tuple2 tryResolveRevertMessage(TransactionReceipt receipt) { + return tryResolveRevertMessage(receipt.getStatus(), receipt.getOutput()); + } +} diff --git a/src/test/java/org/fisco/bcos/channel/test/RevertResolverTest.java b/src/test/java/org/fisco/bcos/channel/test/RevertResolverTest.java new file mode 100644 index 000000000..774913109 --- /dev/null +++ b/src/test/java/org/fisco/bcos/channel/test/RevertResolverTest.java @@ -0,0 +1,239 @@ +package org.fisco.bcos.channel.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import org.fisco.bcos.web3j.abi.FunctionEncoder; +import org.fisco.bcos.web3j.abi.TypeReference; +import org.fisco.bcos.web3j.abi.datatypes.Function; +import org.fisco.bcos.web3j.abi.datatypes.Type; +import org.fisco.bcos.web3j.abi.datatypes.Utf8String; +import org.fisco.bcos.web3j.crypto.EncryptType; +import org.fisco.bcos.web3j.tuples.generated.Tuple2; +import org.fisco.bcos.web3j.tx.RevertResolver; +import org.junit.Test; + +public class RevertResolverTest { + + private Function newFunction(String name, String message) { + return new Function( + name, + Arrays.asList(new org.fisco.bcos.web3j.abi.datatypes.Utf8String(message)), + Collections.singletonList(new TypeReference() {})); + } + + @Test + public void hasRevertMessageTest() throws IOException { + String revertMessage = "RevertMessage"; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + assertFalse(RevertResolver.hasRevertMessage(null, null)); + assertFalse(RevertResolver.hasRevertMessage("", null)); + assertFalse(RevertResolver.hasRevertMessage(null, "")); + assertFalse(RevertResolver.hasRevertMessage("", "")); + assertFalse(RevertResolver.hasRevertMessage("0x0", "")); + assertFalse(RevertResolver.hasRevertMessage("0x0", "")); + assertFalse(RevertResolver.hasRevertMessage("0x0", revertABI)); + assertTrue(RevertResolver.hasRevertMessage("0x1", revertABI)); + assertFalse(RevertResolver.hasRevertMessage(null, revertABI)); + + assertFalse(RevertResolver.hasRevertMessage("0x0", testABI)); + assertFalse(RevertResolver.hasRevertMessage("0x1", testABI)); + assertFalse(RevertResolver.hasRevertMessage(null, testABI)); + } + + @Test + public void hasRevertMessageSMTest() throws IOException { + EncryptType encryptType = new EncryptType(EncryptType.SM2_TYPE); + String revertMessage = "RevertMessage"; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + assertFalse(RevertResolver.hasRevertMessage(null, null)); + assertFalse(RevertResolver.hasRevertMessage("", null)); + assertFalse(RevertResolver.hasRevertMessage(null, "")); + assertFalse(RevertResolver.hasRevertMessage("", "")); + assertFalse(RevertResolver.hasRevertMessage("0x0", "")); + assertFalse(RevertResolver.hasRevertMessage("0x0", "")); + assertFalse(RevertResolver.hasRevertMessage("0x0", revertABI)); + assertTrue(RevertResolver.hasRevertMessage("0x1", revertABI)); + assertFalse(RevertResolver.hasRevertMessage(null, revertABI)); + + assertFalse(RevertResolver.hasRevertMessage("0x0", testABI)); + assertFalse(RevertResolver.hasRevertMessage("0x1", testABI)); + assertFalse(RevertResolver.hasRevertMessage(null, testABI)); + EncryptType encryptType0 = new EncryptType(EncryptType.ECDSA_TYPE); + } + + @Test + public void isOutputStartWithRevertMethodTest() { + String revertMessage = "isOutputStartWithRevertMethodTest"; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + assertTrue(RevertResolver.isOutputStartWithRevertMethod(revertABI)); + assertFalse(RevertResolver.isOutputStartWithRevertMethod(testABI)); + assertTrue(RevertResolver.isOutputStartWithRevertMethod(revertABI)); + assertFalse(RevertResolver.isOutputStartWithRevertMethod(testABI)); + } + + @Test + public void isOutputStartWithRevertMethodSMTest() { + EncryptType encryptType = new EncryptType(EncryptType.SM2_TYPE); + String revertMessage = "isOutputStartWithRevertMethodTest"; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + assertTrue(RevertResolver.isOutputStartWithRevertMethod(revertABI)); + assertFalse(RevertResolver.isOutputStartWithRevertMethod(testABI)); + assertTrue(RevertResolver.isOutputStartWithRevertMethod(revertABI)); + assertFalse(RevertResolver.isOutputStartWithRevertMethod(testABI)); + EncryptType encryptType0 = new EncryptType(EncryptType.ECDSA_TYPE); + } + + @Test + public void tryResolveRevertMessageTest() throws IOException { + String revertMessage = "RevertMessage"; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + Tuple2 booleanStringTuple2 = + RevertResolver.tryResolveRevertMessage("", ""); + assertTrue(!booleanStringTuple2.getValue1()); + + Tuple2 booleanStringTuple20 = + RevertResolver.tryResolveRevertMessage("0x0", revertABI); + assertFalse(booleanStringTuple20.getValue1()); + + Tuple2 booleanStringTuple21 = + RevertResolver.tryResolveRevertMessage("0x0", testABI); + assertFalse(booleanStringTuple21.getValue1()); + + Tuple2 booleanStringTuple22 = + RevertResolver.tryResolveRevertMessage("0x1", testABI); + assertFalse(booleanStringTuple22.getValue1()); + + Tuple2 booleanStringTuple23 = + RevertResolver.tryResolveRevertMessage("0x1", revertABI); + assertTrue(booleanStringTuple23.getValue1()); + assertEquals(booleanStringTuple23.getValue2(), revertMessage); + } + + @Test + public void tryResolveRevertMessageSMTest() throws IOException { + EncryptType encryptType = new EncryptType(EncryptType.SM2_TYPE); + String revertMessage = "RevertMessage"; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + Tuple2 booleanStringTuple2 = + RevertResolver.tryResolveRevertMessage("", ""); + assertFalse(booleanStringTuple2.getValue1()); + + Tuple2 booleanStringTuple20 = + RevertResolver.tryResolveRevertMessage("0x0", revertABI); + assertFalse(booleanStringTuple20.getValue1()); + + Tuple2 booleanStringTuple21 = + RevertResolver.tryResolveRevertMessage("0x0", testABI); + assertFalse(booleanStringTuple21.getValue1()); + + Tuple2 booleanStringTuple22 = + RevertResolver.tryResolveRevertMessage("0x1", testABI); + assertFalse(booleanStringTuple22.getValue1()); + + Tuple2 booleanStringTuple23 = + RevertResolver.tryResolveRevertMessage("0x1", revertABI); + assertTrue(booleanStringTuple23.getValue1()); + assertEquals(booleanStringTuple23.getValue2(), revertMessage); + EncryptType encryptType0 = new EncryptType(EncryptType.ECDSA_TYPE); + } + + @Test + public void tryResolveRevertMessageTest0() throws IOException { + String revertMessage = ""; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + Tuple2 booleanStringTuple2 = + RevertResolver.tryResolveRevertMessage("", ""); + assertFalse(booleanStringTuple2.getValue1()); + + Tuple2 booleanStringTuple20 = + RevertResolver.tryResolveRevertMessage("0x0", revertABI); + assertFalse(booleanStringTuple20.getValue1()); + + Tuple2 booleanStringTuple21 = + RevertResolver.tryResolveRevertMessage("0x0", testABI); + assertFalse(booleanStringTuple21.getValue1()); + + Tuple2 booleanStringTuple22 = + RevertResolver.tryResolveRevertMessage("0x1", testABI); + assertFalse(booleanStringTuple22.getValue1()); + + Tuple2 booleanStringTuple23 = + RevertResolver.tryResolveRevertMessage("0x1", revertABI); + assertTrue(booleanStringTuple23.getValue1()); + assertEquals(booleanStringTuple23.getValue2(), revertMessage); + } + + @Test + public void tryResolveRevertMessageSMTest0() throws IOException { + EncryptType encryptType = new EncryptType(EncryptType.SM2_TYPE); + String revertMessage = ""; + Function revertFunction = newFunction("Error", revertMessage); + String revertABI = FunctionEncoder.encode(revertFunction); + + Function testFunction = newFunction("testFunc", revertMessage); + String testABI = FunctionEncoder.encode(testFunction); + + Tuple2 booleanStringTuple2 = + RevertResolver.tryResolveRevertMessage("", ""); + assertFalse(booleanStringTuple2.getValue1()); + + Tuple2 booleanStringTuple20 = + RevertResolver.tryResolveRevertMessage("0x0", revertABI); + assertFalse(booleanStringTuple20.getValue1()); + + Tuple2 booleanStringTuple21 = + RevertResolver.tryResolveRevertMessage("0x0", testABI); + assertFalse(booleanStringTuple21.getValue1()); + + Tuple2 booleanStringTuple22 = + RevertResolver.tryResolveRevertMessage("0x1", testABI); + assertFalse(booleanStringTuple22.getValue1()); + + Tuple2 booleanStringTuple23 = + RevertResolver.tryResolveRevertMessage("0x1", revertABI); + assertTrue(booleanStringTuple23.getValue1()); + assertEquals(booleanStringTuple23.getValue2(), revertMessage); + + EncryptType encryptType0 = new EncryptType(EncryptType.ECDSA_TYPE); + } +}