diff --git a/TransactionTests.md b/TransactionTests.md new file mode 100644 index 000000000..6cf274b0d --- /dev/null +++ b/TransactionTests.md @@ -0,0 +1,224 @@ +TransactionTests +=== +## ttAddress +```diff ++ AddressLessThan20.json OK ++ AddressLessThan20Prefixed0.json OK ++ AddressMoreThan20.json OK ++ AddressMoreThan20PrefixedBy0.json OK +``` +OK: 4/4 Fail: 0/4 Skip: 0/4 +## ttData +```diff ++ DataTestEnoughGAS.json OK ++ DataTestFirstZeroBytes.json OK ++ DataTestLastZeroBytes.json OK ++ DataTestNotEnoughGAS.json OK ++ DataTestZeroBytes.json OK ++ String10MbData.json OK ++ dataTx_bcValidBlockTest.json OK ++ dataTx_bcValidBlockTestFrontier.json OK +``` +OK: 8/8 Fail: 0/8 Skip: 0/8 +## ttGasLimit +```diff ++ NotEnoughGasLimit.json OK ++ TransactionWithGasLimitOverflow.json OK ++ TransactionWithGasLimitOverflow2.json OK ++ TransactionWithGasLimitOverflow63.json OK ++ TransactionWithGasLimitOverflow63_1.json OK ++ TransactionWithGasLimitxPriceOverflow.json OK ++ TransactionWithGasLimitxPriceOverflow2.json OK ++ TransactionWithHighGas.json OK ++ TransactionWithHihghGasLimit63m1.json OK +``` +OK: 9/9 Fail: 0/9 Skip: 0/9 +## ttGasPrice +```diff ++ TransactionWithGasPriceOverflow.json OK ++ TransactionWithHighGasPrice.json OK ++ TransactionWithHighGasPrice2.json OK +``` +OK: 3/3 Fail: 0/3 Skip: 0/3 +## ttNonce +```diff ++ TransactionWithHighNonce256.json OK ++ TransactionWithHighNonce32.json OK ++ TransactionWithNonceOverflow.json OK +``` +OK: 3/3 Fail: 0/3 Skip: 0/3 +## ttRSValue +```diff ++ RightVRSTestF0000000a.json OK ++ RightVRSTestF0000000b.json OK ++ RightVRSTestF0000000c.json OK ++ RightVRSTestF0000000d.json OK ++ RightVRSTestF0000000e.json OK ++ RightVRSTestF0000000f.json OK ++ RightVRSTestVPrefixedBy0.json OK ++ RightVRSTestVPrefixedBy0_2.json OK ++ RightVRSTestVPrefixedBy0_3.json OK ++ TransactionWithRSvalue0.json OK ++ TransactionWithRSvalue1.json OK ++ TransactionWithRvalue0.json OK ++ TransactionWithRvalue1.json OK ++ TransactionWithRvalueHigh.json OK ++ TransactionWithRvalueOverflow.json OK ++ TransactionWithRvaluePrefixed00.json OK ++ TransactionWithRvalueTooHigh.json OK ++ TransactionWithSvalue0.json OK ++ TransactionWithSvalue1.json OK ++ TransactionWithSvalueEqual_c_secp256k1n_x05.json OK ++ TransactionWithSvalueHigh.json OK ++ TransactionWithSvalueLargerThan_c_secp256k1n_x05.json OK ++ TransactionWithSvalueLessThan_c_secp256k1n_x05.json OK ++ TransactionWithSvalueOverflow.json OK ++ TransactionWithSvaluePrefixed00.json OK ++ TransactionWithSvalueTooHigh.json OK ++ unpadedRValue.json OK +``` +OK: 27/27 Fail: 0/27 Skip: 0/27 +## ttSignature +```diff ++ EmptyTransaction.json OK ++ RSsecp256k1.json OK ++ RightVRSTest.json OK ++ SenderTest.json OK ++ TransactionWithTooFewRLPElements.json OK ++ TransactionWithTooManyRLPElements.json OK ++ Vitalik_1.json OK ++ Vitalik_10.json OK ++ Vitalik_11.json OK ++ Vitalik_12.json OK ++ Vitalik_13.json OK ++ Vitalik_14.json OK ++ Vitalik_15.json OK ++ Vitalik_16.json OK ++ Vitalik_17.json OK ++ Vitalik_2.json OK ++ Vitalik_3.json OK ++ Vitalik_4.json OK ++ Vitalik_5.json OK ++ Vitalik_6.json OK ++ Vitalik_7.json OK ++ Vitalik_8.json OK ++ Vitalik_9.json OK ++ WrongVRSTestIncorrectSize.json OK ++ WrongVRSTestVOverflow.json OK ++ ZeroSigTransaction.json OK ++ ZeroSigTransaction2.json OK ++ ZeroSigTransaction3.json OK ++ ZeroSigTransaction4.json OK ++ ZeroSigTransaction5.json OK ++ ZeroSigTransaction6.json OK ++ invalidSignature.json OK ++ libsecp256k1test.json OK +``` +OK: 33/33 Fail: 0/33 Skip: 0/33 +## ttVValue +```diff ++ V_equals37.json OK ++ V_equals38.json OK ++ V_overflow32bit.json OK ++ V_overflow32bitSigned.json OK ++ V_overflow64bitPlus27.json OK ++ V_overflow64bitPlus28.json OK ++ V_overflow64bitSigned.json OK ++ V_wrongvalue_101.json OK ++ V_wrongvalue_121.json OK ++ V_wrongvalue_122.json OK ++ V_wrongvalue_123.json OK ++ V_wrongvalue_124.json OK ++ V_wrongvalue_ff.json OK ++ V_wrongvalue_ffff.json OK ++ WrongVRSTestVEqual26.json OK ++ WrongVRSTestVEqual29.json OK ++ WrongVRSTestVEqual31.json OK ++ WrongVRSTestVEqual36.json OK ++ WrongVRSTestVEqual39.json OK ++ WrongVRSTestVEqual41.json OK +``` +OK: 20/20 Fail: 0/20 Skip: 0/20 +## ttValue +```diff ++ TransactionWithHighValue.json OK ++ TransactionWithHighValueOverflow.json OK +``` +OK: 2/2 Fail: 0/2 Skip: 0/2 +## ttWrongRLP +```diff ++ RLPAddressWithFirstZeros.json OK ++ RLPAddressWrongSize.json OK ++ RLPArrayLengthWithFirstZeros.json OK ++ RLPElementIsListWhenItShouldntBe.json OK ++ RLPElementIsListWhenItShouldntBe2.json OK ++ RLPExtraRandomByteAtTheEnd.json OK + RLPHeaderSizeOverflowInt32.json Skip ++ RLPIncorrectByteEncoding00.json OK ++ RLPIncorrectByteEncoding01.json OK ++ RLPIncorrectByteEncoding127.json OK ++ RLPListLengthWithFirstZeros.json OK ++ RLPNonceWithFirstZeros.json OK ++ RLPTransactionGivenAsArray.json OK ++ RLPValueWithFirstZeros.json OK ++ RLPWrongAddress.json OK ++ RLPWrongData.json OK ++ RLPgasLimitWithFirstZeros.json OK ++ RLPgasPriceWithFirstZeros.json OK ++ TRANSCT_HeaderGivenAsArray_0.json OK ++ TRANSCT_HeaderLargerThanRLP_0.json OK ++ TRANSCT__RandomByteAtRLP_0.json OK ++ TRANSCT__RandomByteAtRLP_1.json OK ++ TRANSCT__RandomByteAtRLP_2.json OK ++ TRANSCT__RandomByteAtRLP_3.json OK ++ TRANSCT__RandomByteAtRLP_4.json OK ++ TRANSCT__RandomByteAtRLP_5.json OK ++ TRANSCT__RandomByteAtRLP_6.json OK ++ TRANSCT__RandomByteAtRLP_7.json OK ++ TRANSCT__RandomByteAtRLP_8.json OK ++ TRANSCT__RandomByteAtRLP_9.json OK ++ TRANSCT__RandomByteAtTheEnd.json OK ++ TRANSCT__WrongCharAtRLP_0.json OK ++ TRANSCT__WrongCharAtRLP_1.json OK ++ TRANSCT__WrongCharAtRLP_2.json OK ++ TRANSCT__WrongCharAtRLP_3.json OK ++ TRANSCT__WrongCharAtRLP_4.json OK ++ TRANSCT__WrongCharAtRLP_5.json OK ++ TRANSCT__WrongCharAtRLP_6.json OK ++ TRANSCT__WrongCharAtRLP_7.json OK ++ TRANSCT__WrongCharAtRLP_8.json OK ++ TRANSCT__WrongCharAtRLP_9.json OK ++ TRANSCT__ZeroByteAtRLP_0.json OK ++ TRANSCT__ZeroByteAtRLP_1.json OK ++ TRANSCT__ZeroByteAtRLP_2.json OK ++ TRANSCT__ZeroByteAtRLP_3.json OK ++ TRANSCT__ZeroByteAtRLP_4.json OK ++ TRANSCT__ZeroByteAtRLP_5.json OK ++ TRANSCT__ZeroByteAtRLP_6.json OK ++ TRANSCT__ZeroByteAtRLP_7.json OK ++ TRANSCT__ZeroByteAtRLP_8.json OK ++ TRANSCT__ZeroByteAtRLP_9.json OK ++ TRANSCT__ZeroByteAtTheEnd.json OK ++ TRANSCT_data_GivenAsList.json OK ++ TRANSCT_gasLimit_GivenAsList.json OK ++ TRANSCT_gasLimit_Prefixed0000.json OK ++ TRANSCT_gasLimit_TooLarge.json OK ++ TRANSCT_rvalue_GivenAsList.json OK ++ TRANSCT_rvalue_Prefixed0000.json OK ++ TRANSCT_rvalue_TooLarge.json OK ++ TRANSCT_rvalue_TooShort.json OK ++ TRANSCT_svalue_GivenAsList.json OK ++ TRANSCT_svalue_Prefixed0000.json OK ++ TRANSCT_svalue_TooLarge.json OK ++ TRANSCT_to_GivenAsList.json OK ++ TRANSCT_to_Prefixed0000.json OK ++ TRANSCT_to_TooLarge.json OK ++ TRANSCT_to_TooShort.json OK ++ aCrashingRLP.json OK ++ aMalicousRLP.json OK ++ tr201506052141PYTHON.json OK +``` +OK: 69/70 Fail: 0/70 Skip: 1/70 + +---TOTAL--- +OK: 178/179 Fail: 0/179 Skip: 1/179 diff --git a/nimbus/constants.nim b/nimbus/constants.nim index b68c55072..2bfd3256a 100644 --- a/nimbus/constants.nim +++ b/nimbus/constants.nim @@ -54,6 +54,8 @@ const MAX_PREV_HEADER_DEPTH* = 256.toBlockNumber MaxCallDepth* = 1024 + SECPK1_N* = Uint256.fromHex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") + ## Fork specific constants # See: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md diff --git a/nimbus/rpc/p2p.nim b/nimbus/rpc/p2p.nim index bd82cf8eb..58a2ff89c 100644 --- a/nimbus/rpc/p2p.nim +++ b/nimbus/rpc/p2p.nim @@ -54,8 +54,9 @@ proc binarySearchGas(vmState: var BaseVMState, transaction: Transaction, sender: value: value ) var + fork = vmState.blockNumber.toFork hiGas = vmState.gasLimit - loGas = transaction.intrinsicGas + loGas = transaction.intrinsicGas(fork) gasPrice = transaction.gasPrice # TODO: Or zero? proc tryTransaction(vmState: var BaseVMState, gasLimit: GasInt): bool = @@ -73,7 +74,7 @@ proc binarySearchGas(vmState: var BaseVMState, transaction: Transaction, sender: var minVal = vmState.gasLimit - maxVal = transaction.intrinsicGas + maxVal = transaction.intrinsicGas(fork) while loGas - hiGas > tolerance: let midPoint = (loGas + hiGas) div 2 if vmState.tryTransaction(midPoint): diff --git a/nimbus/transaction.nim b/nimbus/transaction.nim index 6b7e7242d..5c06d9f7c 100644 --- a/nimbus/transaction.nim +++ b/nimbus/transaction.nim @@ -6,7 +6,8 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - constants, errors, eth/[common, rlp, keys], nimcrypto, utils + constants, errors, eth/[common, rlp, keys], nimcrypto, utils, + ./vm/interpreter/[vm_forks, gas_costs], constants import eth/common/transaction as common_transaction export common_transaction @@ -19,18 +20,14 @@ func intrinsicGas*(data: openarray[byte]): GasInt = else: result += 68 # GasTXDataNonZero -proc intrinsicGas*(t: Transaction): GasInt = +proc intrinsicGas*(tx: Transaction, fork: Fork): GasInt = # Compute the baseline gas cost for this transaction. This is the amount # of gas needed to send this transaction (but that is not actually used # for computation) - result = t.payload.intrinsicGas + result = tx.payload.intrinsicGas -proc validate*(t: Transaction) = - # Hook called during instantiation to ensure that all transaction - # parameters pass validation rules - if t.intrinsicGas() > t.gasLimit: - raise newException(ValidationError, "Insufficient gas") - # self.check_signature_validity() + if tx.isContractCreation: + result = result + gasFees[fork][GasTXCreate] proc getSignature*(transaction: Transaction, output: var Signature): bool = var bytes: array[65, byte] @@ -51,7 +48,7 @@ proc getSignature*(transaction: Transaction, output: var Signature): bool = proc toSignature*(transaction: Transaction): Signature = if not getSignature(transaction, result): - raise newException(Exception, "Invalid signaure") + raise newException(Exception, "Invalid signature") proc getSender*(transaction: Transaction, output: var EthAddress): bool = ## Find the address the transaction was sent from. @@ -74,3 +71,37 @@ proc getRecipient*(tx: Transaction): EthAddress = result = generateAddress(sender, tx.accountNonce) else: result = tx.to + +proc validate*(tx: Transaction, fork: Fork) = + # Hook called during instantiation to ensure that all transaction + # parameters pass validation rules + if tx.intrinsicGas(fork) > tx.gasLimit: + raise newException(ValidationError, "Insufficient gas") + + # check signature validity + var sender: EthAddress + if not tx.getSender(sender): + raise newException(ValidationError, "Invalid signature or failed message verification") + + var + vMin = 27 + vMax = 28 + + if tx.V.int >= EIP155_CHAIN_ID_OFFSET: + let chainId = (tx.V.int - EIP155_CHAIN_ID_OFFSET) div 2 + vMin = 35 + (2 * chainId) + vMax = vMin + 1 + + var isValid = tx.R >= Uint256.one + isValid = isValid and tx.S >= Uint256.one + isValid = isValid and tx.V.int >= vMin + isValid = isValid and tx.V.int <= vMax + isValid = isValid and tx.S < SECPK1_N + isValid = isValid and tx.R < SECPK1_N + + if fork >= FkHomestead: + isValid = isValid and tx.S < SECPK1_N div 2 + + if not isValid: + raise newException(ValidationError, "Invalid transaction") + diff --git a/nimbus/vm_state_transactions.nim b/nimbus/vm_state_transactions.nim index 3b6349959..f7c19e94f 100644 --- a/nimbus/vm_state_transactions.nim +++ b/nimbus/vm_state_transactions.nim @@ -11,30 +11,29 @@ import constants, errors, transaction, vm_types, vm_state, utils, ./vm/[computation, interpreter], ./vm/interpreter/gas_costs -proc validateTransaction*(vmState: BaseVMState, transaction: Transaction, sender: EthAddress): bool = +proc validateTransaction*(vmState: BaseVMState, tx: Transaction, sender: EthAddress, fork: Fork): bool = # XXX: https://github.com/status-im/nimbus/issues/35#issuecomment-391726518 # XXX: lots of avoidable u256 construction let account = vmState.readOnlyStateDB.getAccount(sender) - gasLimit = transaction.gasLimit.u256 - limitAndValue = gasLimit + transaction.value - gasCost = gasLimit * transaction.gasPrice.u256 + gasLimit = tx.gasLimit.u256 + limitAndValue = gasLimit + tx.value + gasCost = gasLimit * tx.gasPrice.u256 - transaction.gasLimit >= transaction.intrinsicGas and + tx.gasLimit >= tx.intrinsicGas(fork) and #transaction.gasPrice <= (1 shl 34) and limitAndValue <= account.balance and - transaction.accountNonce == account.nonce and + tx.accountNonce == account.nonce and account.balance >= gasCost proc setupComputation*(vmState: BaseVMState, tx: Transaction, sender, recipient: EthAddress, fork: Fork) : BaseComputation = - var gas = tx.gasLimit - tx.intrinsicGas + var gas = tx.gasLimit - tx.intrinsicGas(fork) # TODO: refactor message to use byterange # instead of seq[byte] var data, code: seq[byte] if tx.isContractCreation: - gas = gas - gasFees[fork][GasTXCreate] data = @[] code = tx.payload else: diff --git a/tests/all_tests.nim b/tests/all_tests.nim index dbe1821a7..7ad99f87c 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -23,5 +23,6 @@ import ./test_code_stream, ./test_op_misc, ./test_op_custom, ./test_state_db, - ./test_difficulty + ./test_difficulty, + ./test_transaction_json diff --git a/tests/test_generalstate_json.nim b/tests/test_generalstate_json.nim index db26c9487..5cbfc1469 100644 --- a/tests/test_generalstate_json.nim +++ b/tests/test_generalstate_json.nim @@ -104,7 +104,7 @@ proc testFixtureIndexes(tester: Tester, testStatusIMPL: var TestStatus) = let success = expectedLogsHash == actualLogsHash and obtainedHash == tester.expectedHash tester.dumpDebugData(vmState, sender, gasUsed, success) - if not validateTransaction(vmState, tester.tx, sender): + if not validateTransaction(vmState, tester.tx, sender, tester.fork): vmState.mutateStateDB: # pre-EIP158 (e.g., Byzantium) should ensure currentCoinbase exists # in later forks, don't create at all diff --git a/tests/test_helpers.nim b/tests/test_helpers.nim index 5878c6ce8..0a00e2275 100644 --- a/tests/test_helpers.nim +++ b/tests/test_helpers.nim @@ -13,6 +13,11 @@ import ../nimbus/vm/interpreter/[gas_costs, vm_forks], ../tests/test_generalstate_failing +func revmap(x: Table[Fork, string]): Table[string, Fork] = + result = initTable[string, Fork]() + for k, v in x: + result[v] = k + const # from https://ethereum-tests.readthedocs.io/en/latest/test_types/state_tests.html forkNames* = { @@ -26,6 +31,8 @@ const supportedForks* = {FkFrontier, FkHomestead, FkTangerine, FkSpurious, FkByzantium, FkConstantinople} + nameToFork* = revmap(forkNames) + type Status* {.pure.} = enum OK, Fail, Skip @@ -113,7 +120,10 @@ func failIn32Bits(folder, name: string): bool = "randomStatetest48.json", # OOM in AppVeyor, not on my machine - "randomStatetest36.json" + "randomStatetest36.json", + + # from test_transaction_json + "RLPHeaderSizeOverflowInt32.json" ] func allowedFailInCurrentBuild(folder, name: string): bool = diff --git a/tests/test_transaction_json.nim b/tests/test_transaction_json.nim new file mode 100644 index 000000000..099816ee0 --- /dev/null +++ b/tests/test_transaction_json.nim @@ -0,0 +1,75 @@ +import + unittest, json, os, tables, strformat, strutils, + eth/[common, rlp], + ./test_helpers, ../nimbus/[transaction, utils, errors] + +const + FIXTURE_FORK_SKIPS = ["_info", "rlp", "Constantinople"] + +proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus) + +suite "Transactions tests": + jsonTest("TransactionTests", testFixture) + +proc txHash(tx: Transaction): string = + toLowerAscii($keccakHash(rlp.encode(tx))) + +proc testTxByFork(tx: Transaction, forkData: JsonNode, forkName: string, testStatusIMPL: var TestStatus) = + try: + tx.validate(nameToFork[forkName]) + except ValidationError: + return + + if forkData.len > 0 and "sender" in forkData: + let sender = ethAddressFromHex(forkData["sender"].getStr) + check "hash" in forkData + check tx.txHash == forkData["hash"].getStr + check tx.getSender == sender + +func noHash(fixture: JsonNode): bool = + result = true + for forkName, forkData in fixture: + if forkName notin FIXTURE_FORK_SKIPS: + if forkData.len == 0: return + if "hash" in forkData: return false + +const SKIP_TITLES = [ + "TransactionWithGasLimitxPriceOverflow", + "TransactionWithHighNonce256", + "TransactionWithHighGasPrice", + "V_equals38" + ] + +proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus) = + var + title: string + rlpData: seq[byte] + tx: Transaction + + for key, fixture in node: + title = key + + try: + rlpData = safeHexToSeqByte(fixture["rlp"].getStr) + except ValueError: + # bad rlp bytes + check noHash(fixture) + return + + try: + tx = rlp.decode(rlpData, Transaction) + except RlpTypeMismatch, MalformedRlpError: + # TODO: + # nimbus rlp cannot allow type mismatch + # e.g. uint256 value put into int64 + # so we skip noHash check + # this behavior different compared to + # py-evm, not sure what should we do + if title in SKIP_TITLES: + return + check noHash(fixture) + return + + for forkName, fork in fixture: + if forkName notin FIXTURE_FORK_SKIPS: + testTxByFork(tx, fork, forkName, testStatusIMPL)