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
-- do not store failed contracts bytecode
-- fix contract creation error detection
-- serialize numbers as string
  • Loading branch information
hleb-albau committed May 23, 2018
1 parent c2d26be commit 0b13557
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 51 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 2 additions & 0 deletions common/src/main/kotlin/fund/cyber/common/StringUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ fun String.toSearchHashFormat(): String {
}
return this
}

fun String.isEmptyHexValue() = this == "0x"
14 changes: 8 additions & 6 deletions common/src/main/kotlin/fund/cyber/search/Serialization.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
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
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)!!
45 changes: 23 additions & 22 deletions common/src/main/kotlin/fund/cyber/search/model/ethereum/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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<String> = 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") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package fund.cyber.search.model.ethereum

import com.fasterxml.jackson.annotation.JsonIgnore

/**
* Contains full transaction trace tree.
*/
data class TxTrace(
val rootOperationTrace: OperationTrace
) {

@JsonIgnore
fun isRootOperationFailed() = rootOperationTrace.result is ErroredOperationResult

@JsonIgnore
fun getAllOperationsTraces() = allSuboperations(listOf(rootOperationTrace))

private fun allSuboperations(operations: List<OperationTrace>): List<OperationTrace> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))!!

}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,13 +15,19 @@ 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.*


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.
Expand All @@ -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<Trace>): Map<String, TxTrace> {

Expand Down Expand Up @@ -105,14 +113,14 @@ private fun toOpTraceAsFlattenList(
trace: Trace, tracesTree: Map<Trace, List<Trace>>, 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

Expand All @@ -127,7 +135,7 @@ private fun toOpTraceAsTree(
trace: Trace, tracesTree: Map<Trace, List<Trace>>, isParentFailed: Boolean, depthFromRoot: Int
): OperationTrace {

val operation = convertOperation(trace.action)
val operation = convertOperation(trace)
val result = convertResult(trace)
val childIsParentFailed = isParentFailed || result is ErroredOperationResult

Expand Down Expand Up @@ -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
}

Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]

0 comments on commit 0b13557

Please sign in to comment.