diff --git a/build.gradle b/build.gradle index a75ca21d..cb4ed56e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { ext { kotlinVersion = "1.2.41" kotlinCoroutinesVersion = "0.22.3" - jacksonVersion = "2.9.2" + jacksonVersion = "2.9.5" kafkaVersion = "1.1.0" springKafkaVersion = "2.1.6.RELEASE" cassandraVersion = "3.5.0" diff --git a/cassandra-service/src/main/kotlin/fund/cyber/cassandra/ethereum/repository/UpdateContractSummaryRepository.kt b/cassandra-service/src/main/kotlin/fund/cyber/cassandra/ethereum/repository/UpdateContractSummaryRepository.kt index 56ff0794..98c270c1 100644 --- a/cassandra-service/src/main/kotlin/fund/cyber/cassandra/ethereum/repository/UpdateContractSummaryRepository.kt +++ b/cassandra-service/src/main/kotlin/fund/cyber/cassandra/ethereum/repository/UpdateContractSummaryRepository.kt @@ -10,6 +10,7 @@ import reactor.core.publisher.Mono //todo write small tests for query consistency with object fields. +//todo test BigDecimal saving /** * To archive atomic updates of contract summaries we should use CAS, two-phase commit, serial reads and quorum writes. */ diff --git a/common/src/main/kotlin/fund/cyber/common/StringUtils.kt b/common/src/main/kotlin/fund/cyber/common/StringUtils.kt index 49d67fa8..586f62ba 100644 --- a/common/src/main/kotlin/fund/cyber/common/StringUtils.kt +++ b/common/src/main/kotlin/fund/cyber/common/StringUtils.kt @@ -8,3 +8,5 @@ fun String.toSearchHashFormat(): String { } return this } + +fun String.isEmptyHexValue() = this == "0x" diff --git a/common/src/main/kotlin/fund/cyber/search/Serialization.kt b/common/src/main/kotlin/fund/cyber/search/Serialization.kt index f9dc2d94..053eca64 100644 --- a/common/src/main/kotlin/fund/cyber/search/Serialization.kt +++ b/common/src/main/kotlin/fund/cyber/search/Serialization.kt @@ -1,6 +1,7 @@ package fund.cyber.search import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jdk8.Jdk8Module @@ -8,11 +9,12 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.registerKotlinModule val jsonSerializer = ObjectMapper().registerKotlinModule() - .registerModule(Jdk8Module()) - .registerModule(JavaTimeModule()) - .setSerializationInclusion(JsonInclude.Include.NON_NULL)!! + .registerModule(Jdk8Module()) + .registerModule(JavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .enable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS)!! val jsonDeserializer = ObjectMapper().registerKotlinModule() - .registerModule(Jdk8Module()) - .registerModule(JavaTimeModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)!! + .registerModule(Jdk8Module()) + .registerModule(JavaTimeModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)!! diff --git a/common/src/main/kotlin/fund/cyber/search/model/ethereum/Block.kt b/common/src/main/kotlin/fund/cyber/search/model/ethereum/Block.kt index 6cd7984d..e5735bf4 100644 --- a/common/src/main/kotlin/fund/cyber/search/model/ethereum/Block.kt +++ b/common/src/main/kotlin/fund/cyber/search/model/ethereum/Block.kt @@ -10,30 +10,31 @@ const val ETHEREUM_CLASSIC_REWARD_CHANGED_BLOCK_NUMBER = 5000000 const val ETHEREUM_REWARD_CHANGED_BLOCK_NUMBER = 4370000 data class EthereumBlock( - override val number: Long, //parsed from hex - val hash: String, - val parentHash: String, - val timestamp: Instant, - val sha3Uncles: String, - val logsBloom: String, - val transactionsRoot: String, - val stateRoot: String, - val receiptsRoot: String, - val minerContractHash: String, - val nonce: Long, //parsed from hex - val difficulty: BigInteger, - val totalDifficulty: BigInteger, //parsed from hex - val extraData: String, - val size: Long, //parsed from hex - val gasLimit: Long, //parsed from hex - val gasUsed: Long, //parsed from hex - val txNumber: Int, - val uncles: List = emptyList(), - val blockReward: BigDecimal, - val unclesReward: BigDecimal, - val txFees: BigDecimal + override val number: Long, //parsed from hex + val hash: String, + val parentHash: String, + val timestamp: Instant, + val sha3Uncles: String, + val logsBloom: String, + val transactionsRoot: String, + val stateRoot: String, + val receiptsRoot: String, + val minerContractHash: String, + val nonce: Long, //parsed from hex + val difficulty: BigInteger, + val totalDifficulty: BigInteger, //parsed from hex + val extraData: String, + val size: Long, //parsed from hex + val gasLimit: Long, //parsed from hex + val gasUsed: Long, //parsed from hex + val txNumber: Int, + val uncles: List = emptyList(), + val blockReward: BigDecimal, + val unclesReward: BigDecimal, + val txFees: BigDecimal ) : BlockEntity +//todo change to use block trace rewards operations //todo: 1) add properly support of new classic fork. 2) add support of custom reward functions in forks fun getBlockReward(chainInfo: ChainInfo, number: Long): BigDecimal { return if (chainInfo.fullName == "ETHEREUM_CLASSIC") { diff --git a/common/src/main/kotlin/fund/cyber/search/model/ethereum/TxTrace.kt b/common/src/main/kotlin/fund/cyber/search/model/ethereum/TxTrace.kt index 673b375a..2be20fa0 100644 --- a/common/src/main/kotlin/fund/cyber/search/model/ethereum/TxTrace.kt +++ b/common/src/main/kotlin/fund/cyber/search/model/ethereum/TxTrace.kt @@ -1,5 +1,7 @@ package fund.cyber.search.model.ethereum +import com.fasterxml.jackson.annotation.JsonIgnore + /** * Contains full transaction trace tree. */ @@ -7,8 +9,10 @@ data class TxTrace( val rootOperationTrace: OperationTrace ) { + @JsonIgnore fun isRootOperationFailed() = rootOperationTrace.result is ErroredOperationResult + @JsonIgnore fun getAllOperationsTraces() = allSuboperations(listOf(rootOperationTrace)) private fun allSuboperations(operations: List): List { diff --git a/common/src/test/kotlin/fund/cyber/node/common/JsonSerializationTest.kt b/common/src/test/kotlin/fund/cyber/node/common/JsonSerializationTest.kt new file mode 100644 index 00000000..b91e68a7 --- /dev/null +++ b/common/src/test/kotlin/fund/cyber/node/common/JsonSerializationTest.kt @@ -0,0 +1,30 @@ +package fund.cyber.node.common + +import fund.cyber.search.jsonDeserializer +import fund.cyber.search.jsonSerializer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +@DisplayName("Json serialization/deserialization test") +class JsonSerializationTest { + + @Test + @DisplayName("should BigDecimal serialized to Json string") + fun testBigDecimalShouldBeSerializedAsJsonString() { + + val valueAsString = "0.32784291897287934505879230273459823424" + val classWithMoney = ClassWithMoney(BigDecimal(valueAsString)) + val classAsString = jsonSerializer.writeValueAsString(classWithMoney) + val deserializedClass = jsonDeserializer.readValue(classAsString, ClassWithMoney::class.java) + + Assertions.assertEquals("""{"money":"0.32784291897287934505879230273459823424"}""", classAsString) + Assertions.assertEquals(classWithMoney, deserializedClass) + } +} + + +private data class ClassWithMoney( + val money: BigDecimal +) diff --git a/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/EthereumClientConfiguration.kt b/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/EthereumClientConfiguration.kt index 66c816b0..08f16122 100644 --- a/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/EthereumClientConfiguration.kt +++ b/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/EthereumClientConfiguration.kt @@ -7,8 +7,8 @@ import org.apache.http.message.BasicHeader import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.web3j.protocol.parity.Parity import org.web3j.protocol.http.HttpService +import org.web3j.protocol.parity.Parity const val MAX_PER_ROUTE = 16 const val MAX_TOTAL = 32 @@ -27,12 +27,11 @@ class EthereumClientConfiguration { @Bean fun httpClient() = HttpClients.custom() - .setConnectionManager(connectionManager) - .setConnectionManagerShared(true) - .setDefaultHeaders(defaultHttpHeaders) - .build()!! + .setConnectionManager(connectionManager) + .setConnectionManagerShared(true) + .setDefaultHeaders(defaultHttpHeaders) + .build()!! @Bean fun parityClient() = Parity.build(HttpService(chainInfo.nodeUrl))!! - } diff --git a/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/TxExecutionTraceConverter.kt b/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/TxExecutionTraceConverter.kt index 183cc7d1..9a17a49b 100644 --- a/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/TxExecutionTraceConverter.kt +++ b/pumps/ethereum/src/main/kotlin/fund/cyber/pump/ethereum/client/TxExecutionTraceConverter.kt @@ -1,6 +1,7 @@ package fund.cyber.pump.ethereum.client import fund.cyber.common.hexToLong +import fund.cyber.common.isEmptyHexValue import fund.cyber.search.model.ethereum.CallOperation import fund.cyber.search.model.ethereum.CallOperationResult import fund.cyber.search.model.ethereum.CreateContractOperation @@ -14,6 +15,10 @@ import fund.cyber.search.model.ethereum.RewardOperation import fund.cyber.search.model.ethereum.TxTrace import fund.cyber.search.model.ethereum.weiToEthRate import org.web3j.protocol.parity.methods.response.Trace +import org.web3j.protocol.parity.methods.response.Trace.CallAction +import org.web3j.protocol.parity.methods.response.Trace.CreateAction +import org.web3j.protocol.parity.methods.response.Trace.RewardAction +import org.web3j.protocol.parity.methods.response.Trace.SuicideAction import java.math.BigDecimal import java.util.* @@ -21,6 +26,8 @@ import java.util.* const val MAX_TRACE_DEPTH = 5 const val SUBTRACES_NUMBER_BEFORE_ZIPPING = 14 +const val CREATE_CONTRACT_ERROR = "Contract creation error" + /** * Trace(parity) is result of single "operation/call" inside transaction (ex: send eth to address inside smart contract * method execution). Parity return all traces for block(tx) as flatten list. @@ -32,9 +39,10 @@ const val SUBTRACES_NUMBER_BEFORE_ZIPPING = 14 * * * IMPORTANT NOTE: - * We do not store all traces. All errored traces, deeper than [MAX_TRACE_DEPTH], will be removed. - * All traces deeper than [MAX_TRACE_DEPTH], will be flattened into single sublist. - * Also, if node have more than [SUBTRACES_NUMBER_BEFORE_ZIPPING] subtraces, all errored will be removed. + * 1) We do not store all traces. All errored traces, deeper than [MAX_TRACE_DEPTH], will be removed. + * 2) All traces deeper than [MAX_TRACE_DEPTH], will be flattened into single sublist. + * 3) Also, if node have more than [SUBTRACES_NUMBER_BEFORE_ZIPPING] subtraces, all errored will be removed. + * 4) We do not store byte code for not created smart contract. */ fun toTxesTraces(parityTraces: List): Map { @@ -105,14 +113,14 @@ private fun toOpTraceAsFlattenList( trace: Trace, tracesTree: Map>, isParentFailed: Boolean ): OperationTrace { - val operation = convertOperation(trace.action) + val operation = convertOperation(trace) val result = convertResult(trace) return if (isParentFailed) { OperationTrace(operation, result, emptyList(), getSubtracesNumber(trace, tracesTree)) } else { val subtracesToStore = getAllSuccessfulSubtracesAsFlattenList(trace, tracesTree).map { subtrace -> - OperationTrace(convertOperation(subtrace.action), convertResult(subtrace)) + OperationTrace(convertOperation(subtrace), convertResult(subtrace)) } val droppedTracesNumber = getSubtracesNumber(trace, tracesTree) - subtracesToStore.size @@ -127,7 +135,7 @@ private fun toOpTraceAsTree( trace: Trace, tracesTree: Map>, isParentFailed: Boolean, depthFromRoot: Int ): OperationTrace { - val operation = convertOperation(trace.action) + val operation = convertOperation(trace) val result = convertResult(trace) val childIsParentFailed = isParentFailed || result is ErroredOperationResult @@ -173,35 +181,49 @@ private fun convertResult(trace: Trace): OperationResult? { val result = trace.result val gasUsed = result.gasUsedRaw.hexToLong() return when (trace.action) { - is Trace.CallAction -> CallOperationResult(gasUsed, result.output) - is Trace.CreateAction -> CreateContractOperationResult(result.address, result.code, gasUsed) + is CallAction -> CallOperationResult(gasUsed, result.output) + is CreateAction -> { + if (trace.isFailed()) ErroredOperationResult(CREATE_CONTRACT_ERROR) + else CreateContractOperationResult(result.address, result.code, gasUsed) + } else -> throw RuntimeException("Unknown trace call result") } } -private fun convertOperation(action: Trace.Action): Operation { +private fun convertOperation(trace: Trace): Operation { + val action = trace.action return when (action) { - is Trace.CallAction -> { + is CallAction -> { CallOperation( type = action.callType, from = action.from, to = action.to, input = action.input, value = BigDecimal(action.value) * weiToEthRate, gasLimit = action.gasRaw.hexToLong() ) } - is Trace.CreateAction -> { + is CreateAction -> { CreateContractOperation( - from = action.from, init = action.init, + from = action.from, init = if (trace.isFailed()) "" else action.init, value = BigDecimal(action.value) * weiToEthRate, gasLimit = action.gasRaw.hexToLong() ) } - is Trace.SuicideAction -> { + is SuicideAction -> { DestroyContractOperation( contractToDestroy = action.address, refundContract = action.refundAddress, refundValue = BigDecimal(action.balance) * weiToEthRate ) } - is Trace.RewardAction -> { + is RewardAction -> { RewardOperation(action.author, BigDecimal(action.value) * weiToEthRate, action.rewardType) } else -> throw RuntimeException("Unknown trace call") } } + +/** + * returns "0x" for contract as indicator of failed contract creation + */ +fun Trace.isFailed(): Boolean = when { + error != null && error.isNotEmpty() -> true + action is CreateAction && (result.code == null || result.code.isEmpty() || result.code.isEmptyHexValue()) -> true + else -> false +} + diff --git a/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceConstructedForMethodCallInvokingContractCreationTest.kt b/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceConstructedForMethodCallInvokingContractCreationTest.kt index 3ad3a6b4..78d80057 100644 --- a/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceConstructedForMethodCallInvokingContractCreationTest.kt +++ b/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceConstructedForMethodCallInvokingContractCreationTest.kt @@ -1,9 +1,11 @@ package fund.cyber.pump.ethereum.client.trace +import fund.cyber.pump.ethereum.client.CREATE_CONTRACT_ERROR import fund.cyber.search.model.ethereum.CallOperation import fund.cyber.search.model.ethereum.CallOperationResult import fund.cyber.search.model.ethereum.CreateContractOperation import fund.cyber.search.model.ethereum.CreateContractOperationResult +import fund.cyber.search.model.ethereum.ErroredOperationResult import fund.cyber.search.model.ethereum.OperationTrace import fund.cyber.search.model.ethereum.TxTrace import org.junit.jupiter.api.Assertions @@ -41,13 +43,14 @@ class TxTraceConstructedForMethodCallInvokingContractCreationTest : BaseTxTraceC val expectedOperationResult = CallOperationResult(output = "0x", gasUsed = 394988) val rootTrace = OperationTrace( - expectedOperation, expectedOperationResult, listOf(createContractSuboperationTrace()) + expectedOperation, expectedOperationResult, + listOf(okCreateContractSuboperationTrace(), failedCreateContractSuboperationTrace()) ) return TxTrace(rootTrace) } - private fun createContractSuboperationTrace(): OperationTrace { + private fun okCreateContractSuboperationTrace(): OperationTrace { val expectedOperation = CreateContractOperation( from = "0x6090a6e47849629b7245dfa1ca21d94cd15878ef", gasLimit = 436209, @@ -62,4 +65,17 @@ class TxTraceConstructedForMethodCallInvokingContractCreationTest : BaseTxTraceC return OperationTrace(expectedOperation, expectedOperationResult, emptyList()) } + + private fun failedCreateContractSuboperationTrace(): OperationTrace { + + val expectedOperation = CreateContractOperation( + from = "0x6090a6e47849629b7245dfa1ca21d94cd15878ef", gasLimit = 436209, + value = BigDecimal("1.030000000000000000"), + init = "" + ) + + val expectedOperationResult = ErroredOperationResult(CREATE_CONTRACT_ERROR) + + return OperationTrace(expectedOperation, expectedOperationResult, emptyList()) + } } diff --git a/pumps/ethereum/src/test/resources/client/traces/0xbec2a4c10d10d657cb57f5790846025d2da271b10cdb929850831bba3a1ef62c.json b/pumps/ethereum/src/test/resources/client/traces/0xbec2a4c10d10d657cb57f5790846025d2da271b10cdb929850831bba3a1ef62c.json index f37b0833..1da0a81a 100644 --- a/pumps/ethereum/src/test/resources/client/traces/0xbec2a4c10d10d657cb57f5790846025d2da271b10cdb929850831bba3a1ef62c.json +++ b/pumps/ethereum/src/test/resources/client/traces/0xbec2a4c10d10d657cb57f5790846025d2da271b10cdb929850831bba3a1ef62c.json @@ -41,5 +41,27 @@ "transactionHash": "0xbec2a4c10d10d657cb57f5790846025d2da271b10cdb929850831bba3a1ef62c", "transactionPosition": 60, "type": "create" + }, + { + "action": { + "from": "0x6090a6e47849629b7245dfa1ca21d94cd15878ef", + "gas": "0x6a7f1", + "init": "0x606060405236156100885763ffffffff60e060020a60003504166305b34410811461008a5780630b5ab3d5146100", + "value": "0xe4b4b8af6a70000" + }, + "blockHash": "0x5d08e5d3858ece6ce9e3d1b81bc95f556c884b2c8f3a8d49a966f3c42bf8b92d", + "blockNumber": 4000023, + "result": { + "address": "0xe8a3c515fd7d0914dc56a4e6dbbb77a7f58bc6ce", + "code": "0x", + "gasUsed": "0x52ce0" + }, + "subtraces": 0, + "traceAddress": [ + 1 + ], + "transactionHash": "0xbec2a4c10d10d657cb57f5790846025d2da271b10cdb929850831bba3a1ef62c", + "transactionPosition": 60, + "type": "create" } ] \ No newline at end of file