From bd5b8ea826b418280ef445711e0ede07600c082b Mon Sep 17 00:00:00 2001 From: Hleb Albau Date: Tue, 22 May 2018 14:35:22 +0300 Subject: [PATCH] #132 Wrong ETH Contract Balance -- limit trace tree depth to 5 --- .../search/model/ethereum/OperationTrace.kt | 9 +- .../client/TxExecutionTraceConverter.kt | 92 +++++++++- .../client/trace/BaseTxTraceConverterTest.kt | 4 +- .../client/trace/TxTraceStackOverflowTest.kt | 157 ++++++++++++++++++ 4 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceStackOverflowTest.kt diff --git a/common/src/main/kotlin/fund/cyber/search/model/ethereum/OperationTrace.kt b/common/src/main/kotlin/fund/cyber/search/model/ethereum/OperationTrace.kt index b49bf6ab..0110d403 100644 --- a/common/src/main/kotlin/fund/cyber/search/model/ethereum/OperationTrace.kt +++ b/common/src/main/kotlin/fund/cyber/search/model/ethereum/OperationTrace.kt @@ -13,14 +13,21 @@ import fund.cyber.search.model.ethereum.OperationType.REWARD import java.lang.RuntimeException +/** + * Represent single operation/call trace. + * See [fund.cyber.pump.ethereum.client.toTxesTraces] function docs. + */ data class OperationTrace( @JsonDeserialize(using = OperationsDeserializer::class) val operation: Operation, @JsonDeserialize(using = OperationResultDeserializer::class) val result: OperationResult?, //null for reward and destroy contract operations - val subtraces: List = emptyList() + val subtraces: List = emptyList(), + val droppedSuboperationsNumber: Int = 0 ) { + fun isOperationFailed() = result is ErroredOperationResult + /** * Do not returns child contracts. */ 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 e73b22ba..183cc7d1 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 @@ -17,6 +17,10 @@ import org.web3j.protocol.parity.methods.response.Trace import java.math.BigDecimal import java.util.* + +const val MAX_TRACE_DEPTH = 5 +const val SUBTRACES_NUMBER_BEFORE_ZIPPING = 14 + /** * 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. @@ -26,6 +30,11 @@ import java.util.* * For method execution txes, first(root) operation(call) duplicate parent tx data (such as value, from, to, etc). * For sm creation/deletion first(root) operation(call) duplicate parent tx data (such as value, from, to, etc). * + * + * 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. */ fun toTxesTraces(parityTraces: List): Map { @@ -64,22 +73,98 @@ private fun toTxTrace(traces: List): TxTrace { parents.push(trace) } - val rootOperationTrace = toOperationTrace(traces[0], tree) + val rootOperationTrace = toOperationTrace(traces[0], tree, 0) return TxTrace(rootOperationTrace) } /** * Converts raw parity trace and its child to search OperationTrace data class. + * + * IMPORTANT NOTE: * !!Recursive by child first. + * !!All traces deeper than [MAX_TRACE_DEPTH], will be flattened into single sublist. + * */ -private fun toOperationTrace(trace: Trace, tracesTree: Map>): OperationTrace { - val subtraces = tracesTree[trace]?.map { subtrace -> toOperationTrace(subtrace, tracesTree) } ?: emptyList() +private fun toOperationTrace( + trace: Trace, tracesTree: Map>, depthFromRoot: Int, isParentFailed: Boolean = false +): OperationTrace { + + // -1 due include root + return if (depthFromRoot < MAX_TRACE_DEPTH - 1) { + toOpTraceAsTree(trace, tracesTree, isParentFailed, depthFromRoot) + } else { + toOpTraceAsFlattenList(trace, tracesTree, isParentFailed) + } +} + + +/** + * All errored traces with their subtraces will be not returned. + */ +private fun toOpTraceAsFlattenList( + trace: Trace, tracesTree: Map>, isParentFailed: Boolean +): OperationTrace { + val operation = convertOperation(trace.action) 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)) + } + val droppedTracesNumber = getSubtracesNumber(trace, tracesTree) - subtracesToStore.size + + OperationTrace(operation, result, subtracesToStore, droppedTracesNumber) + } +} + +/** + * If node have more than [SUBTRACES_NUMBER_BEFORE_ZIPPING] subtraces, all errored one will be removed. + */ +private fun toOpTraceAsTree( + trace: Trace, tracesTree: Map>, isParentFailed: Boolean, depthFromRoot: Int +): OperationTrace { + + val operation = convertOperation(trace.action) + val result = convertResult(trace) + val childIsParentFailed = isParentFailed || result is ErroredOperationResult + + val children = tracesTree[trace] ?: return OperationTrace(operation, result, emptyList()) + + val subtraces = children.map { subtrace -> + toOperationTrace(subtrace, tracesTree, depthFromRoot + 1, childIsParentFailed) + } + if (subtraces.size > SUBTRACES_NUMBER_BEFORE_ZIPPING) { + val subtracesToStore = subtraces.filterNot(OperationTrace::isOperationFailed) + val droppedTracesNumber = subtraces.size - subtracesToStore.size + return OperationTrace(operation, result, subtracesToStore, droppedTracesNumber) + } return OperationTrace(operation, result, subtraces) } +private fun getSubtracesNumber(trace: Trace, tracesTree: Map>): Int { + return tracesTree[trace]?.map { subtrace -> getSubtracesNumber(subtrace, tracesTree) + 1 }?.sum() ?: 0 +} + + +private fun getAllSuccessfulSubtracesAsFlattenList( + trace: Trace, tracesTree: Map> +): List { + + val children = tracesTree[trace] ?: emptyList() + return children.flatMap { subtrace -> + if (subtrace.error == null || subtrace.error.isEmpty()) { + listOf(subtrace) + getAllSuccessfulSubtracesAsFlattenList(subtrace, tracesTree) + } else { + emptyList() + } + } +} + + private fun convertResult(trace: Trace): OperationResult? { if (trace.error != null && trace.error.isNotEmpty()) return ErroredOperationResult(trace.error) @@ -94,7 +179,6 @@ private fun convertResult(trace: Trace): OperationResult? { } } -//todo check weiToEthRate value result private fun convertOperation(action: Trace.Action): Operation { return when (action) { is Trace.CallAction -> { diff --git a/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/BaseTxTraceConverterTest.kt b/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/BaseTxTraceConverterTest.kt index bb9ef656..26b6ff27 100644 --- a/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/BaseTxTraceConverterTest.kt +++ b/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/BaseTxTraceConverterTest.kt @@ -13,9 +13,9 @@ abstract class BaseTxTraceConverterTest { return toTxesTraces(flattenTraces)[txHash]!! } - protected fun getParityTracesForTx(txHash: String): List { + protected fun getParityTracesForTx(txHash: String): MutableList { val tracesResourceLocation = javaClass.getResource("/client/traces/$txHash.json") - return jsonDeserializer.readValue(tracesResourceLocation, Array::class.java).toList() + return jsonDeserializer.readValue(tracesResourceLocation, Array::class.java).toMutableList() } } diff --git a/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceStackOverflowTest.kt b/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceStackOverflowTest.kt new file mode 100644 index 00000000..33a0fdd5 --- /dev/null +++ b/pumps/ethereum/src/test/kotlin/fund/cyber/pump/ethereum/client/trace/TxTraceStackOverflowTest.kt @@ -0,0 +1,157 @@ +@file:Suppress("LocalVariableName") + +package fund.cyber.pump.ethereum.client.trace + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import fund.cyber.pump.ethereum.client.toTxesTraces +import fund.cyber.search.jsonSerializer +import fund.cyber.search.model.ethereum.ErroredOperationResult +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.web3j.protocol.parity.methods.response.Trace +import java.math.BigInteger.ONE +import java.math.BigInteger.ZERO + +/** + * Test tx trace should reduce stackoverlow brances to only parent. + */ +class TxTraceStackOverflowTest : BaseTxTraceConverterTest() { + + val txHash = "0x47bfefd0b1e6c055391e7b091425762322dde55ff72a9d3567b3c22a78b4a38a" + + @Test + @DisplayName("All traces deeper than [MAX_TRACE_DEPTH], should be flattened into single sublist") + fun testAllTracesDeeperThanLimitShouldBeFlattenIntoSingleParentSubtracesList() { + + val flattenTraces = getFlattenTracesWithDeepStack() + val txTrace = toTxesTraces(flattenTraces)[txHash]!! + val traceFor_0_0_0_0 = txTrace.rootOperationTrace.subtraces[0].subtraces[0].subtraces[0].subtraces[0] + + val txTraceAsString = jsonSerializer.writeValueAsString(txTrace) + + Assertions.assertEquals(1020, txTrace.getAllOperationsTraces().size) + Assertions.assertEquals(12, txTrace.rootOperationTrace.subtraces.size) + Assertions.assertTrue(txTrace.rootOperationTrace.subtraces[8].result is ErroredOperationResult) + Assertions.assertEquals(998, traceFor_0_0_0_0.subtraces.size) + } + + @Test + @DisplayName("All errored traces, deeper than [MAX_TRACE_DEPTH], should be removed") + fun testFailedTracesDeeperThanLimitShouldBeRemoved() { + + val flattenTraces = getFlattenTracesWithDeepFailedStack() + val txTrace = toTxesTraces(flattenTraces)[txHash]!! + val traceFor_0_0_0_0 = txTrace.rootOperationTrace.subtraces[0].subtraces[0].subtraces[0].subtraces[0] + val traceFor_0_0_0_1 = txTrace.rootOperationTrace.subtraces[0].subtraces[0].subtraces[0].subtraces[1] + + val txTraceAsString = jsonSerializer.writeValueAsString(txTrace) + + // final tree should be: + // root-0-0-(0 FAILED) + // | -> 0 OK -> 199 dropped calls + // | -> 0 FAILED -> 998 dropped calls + // + Assertions.assertEquals(23, txTrace.getAllOperationsTraces().size) + Assertions.assertEquals(12, txTrace.rootOperationTrace.subtraces.size) + Assertions.assertEquals(0, traceFor_0_0_0_0.subtraces.size) + Assertions.assertEquals(0, traceFor_0_0_0_1.subtraces.size) + Assertions.assertEquals(199, traceFor_0_0_0_0.droppedSuboperationsNumber) + Assertions.assertEquals(998, traceFor_0_0_0_1.droppedSuboperationsNumber) + } + + @Test + @DisplayName("If call have more than [SUBTRACES_NUMBER_BEFORE_ZIPPING] subtraces, all errored should be removed") + fun testZipSubtracesForTraceWithALotOfSubtraces() { + + val flattenTraces = getFlattenTracesWithWideStack() + val txTrace = toTxesTraces(flattenTraces)[txHash]!! + val traceFor_0_0 = txTrace.rootOperationTrace.subtraces[0].subtraces[0] + + val txTraceAsString = jsonSerializer.writeValueAsString(txTrace) + + // final tree should be: + // root-0-0 + // \ -> 500 sub + 500 dropped + // + Assertions.assertEquals(520, txTrace.getAllOperationsTraces().size) + Assertions.assertEquals(12, txTrace.rootOperationTrace.subtraces.size) + Assertions.assertEquals(500, traceFor_0_0.subtraces.size) + Assertions.assertEquals(500, traceFor_0_0.droppedSuboperationsNumber) + } + + + private fun getFlattenTracesWithDeepStack(): List { + val flattenTraces = getParityTracesForTx(txHash) + val rootCall = flattenTraces.first() + + val subtracesFor_0_0 = (1..1000).map { depth -> + return@map mock { + on { action } doReturn rootCall.action + on { transactionHash } doReturn txHash + on { result } doReturn rootCall.result + on { subtraces } doReturn ONE + on { traceAddress } doReturn Array(depth + 2, { ZERO }).toList() + } + } + flattenTraces.addAll(3, subtracesFor_0_0) + return flattenTraces + } + + + private fun getFlattenTracesWithDeepFailedStack(): List { + val flattenTraces = getParityTracesForTx(txHash) + val rootCall = flattenTraces.first() + + //failed subtraces for root-0-0 call + val subtracesFor_0_0 = (1..1000).map { depth -> + return@map mock { + on { action } doReturn rootCall.action + on { transactionHash } doReturn txHash + on { error } doReturn if (depth % 100 == 0) null else "Reverted" + on { result } doReturn if (depth % 100 == 0) rootCall.result else null + on { subtraces } doReturn ONE + on { traceAddress } doReturn Array(depth + 2, { ZERO }).toList() + } + } + + // ok subtraces for root-0-0-0 call + // root-0-0-0 is call inserted previously + // final tree is root-0-0-0 + // | -> 999 failed recursive calls + // | -> 200 ok recursive calls + val subtracesFor_0_0_0 = (1..200).map { depth -> + return@map mock { + on { action } doReturn rootCall.action + on { transactionHash } doReturn txHash + on { result } doReturn rootCall.result + on { subtraces } doReturn ONE + on { traceAddress } doReturn Array(depth + 3, { ZERO }).toList() + } + } + flattenTraces.addAll(3, subtracesFor_0_0) + flattenTraces.addAll(4, subtracesFor_0_0_0) + return flattenTraces + } + + + private fun getFlattenTracesWithWideStack(): List { + val flattenTraces = getParityTracesForTx(txHash) + val rootCall = flattenTraces.first() + + // ok/failed subtraces for root-0-0 call + val subtracesFor_0_0 = (0..999).map { depth -> + return@map mock { + on { action } doReturn rootCall.action + on { transactionHash } doReturn txHash + on { error } doReturn if (depth % 2 == 0) null else "Reverted" + on { result } doReturn if (depth % 2 == 0) rootCall.result else null + on { subtraces } doReturn ONE + on { traceAddress } doReturn listOf(ZERO, ZERO, depth.toBigInteger()) + } + } + flattenTraces.addAll(3, subtracesFor_0_0) + return flattenTraces + } +}