Skip to content
This repository has been archived by the owner on Jan 29, 2019. It is now read-only.

Commit

Permalink
#132 Wrong ETH Contract Balance
Browse files Browse the repository at this point in the history
-- limit trace tree depth to 5
  • Loading branch information
hleb-albau committed May 22, 2018
1 parent faf3be7 commit bd5b8ea
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperationTrace> = emptyList()
val subtraces: List<OperationTrace> = emptyList(),
val droppedSuboperationsNumber: Int = 0
) {

fun isOperationFailed() = result is ErroredOperationResult

/**
* Do not returns child contracts.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Trace>): Map<String, TxTrace> {

Expand Down Expand Up @@ -64,22 +73,98 @@ private fun toTxTrace(traces: List<Trace>): 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<Trace, List<Trace>>): OperationTrace {
val subtraces = tracesTree[trace]?.map { subtrace -> toOperationTrace(subtrace, tracesTree) } ?: emptyList()
private fun toOperationTrace(
trace: Trace, tracesTree: Map<Trace, List<Trace>>, 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<Trace, List<Trace>>, 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<Trace, List<Trace>>, 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<Trace, List<Trace>>): Int {
return tracesTree[trace]?.map { subtrace -> getSubtracesNumber(subtrace, tracesTree) + 1 }?.sum() ?: 0
}


private fun getAllSuccessfulSubtracesAsFlattenList(
trace: Trace, tracesTree: Map<Trace, List<Trace>>
): List<Trace> {

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)
Expand All @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ abstract class BaseTxTraceConverterTest {
return toTxesTraces(flattenTraces)[txHash]!!
}

protected fun getParityTracesForTx(txHash: String): List<Trace> {
protected fun getParityTracesForTx(txHash: String): MutableList<Trace> {

val tracesResourceLocation = javaClass.getResource("/client/traces/$txHash.json")
return jsonDeserializer.readValue(tracesResourceLocation, Array<Trace>::class.java).toList()
return jsonDeserializer.readValue(tracesResourceLocation, Array<Trace>::class.java).toMutableList()
}
}
Original file line number Diff line number Diff line change
@@ -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<Trace> {
val flattenTraces = getParityTracesForTx(txHash)
val rootCall = flattenTraces.first()

val subtracesFor_0_0 = (1..1000).map { depth ->
return@map mock<Trace> {
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<Trace> {
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<Trace> {
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<Trace> {
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<Trace> {
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<Trace> {
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
}
}

0 comments on commit bd5b8ea

Please sign in to comment.