Skip to content

Commit a7a62c7

Browse files
author
Aurélien Richez
authored
[ETCM-878] make BlockchainTests/vmArithmeticTest pass (#1002)
partially fix getAccountsInRange partially fix debug_storageRangeAt Add some documentation about ETS Delete database on startup when running in test mode
1 parent 71c1545 commit a7a62c7

16 files changed

+425
-107
lines changed

ets/README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,22 @@ system. First, find the IP it should use:
4444

4545
nix-in-docker/run --command "getent hosts host.docker.internal"
4646

47-
Edit `ets/config/mantis` and replace `0.0.0.0` with this IP.
48-
4947
Finally, run retesteth in Nix in Docker:
5048

51-
nix-in-docker/run --command "ets/retesteth -t GeneralStateTests"
49+
nix-in-docker/run --command "ets/retesteth -t GeneralStateTests -- --nodes <ip>:8546"
50+
51+
## Useful options:
52+
53+
You can run one test by selecting one suite and using `--singletest`, for instance:
54+
55+
nix-in-docker/run -t BlockchainTests/ValidBlocks/VMTests/vmArithmeticTest -- --nodes <ip>:8546 --singletest add0"
56+
57+
However it's not always clear in wich subfolder the suite is when looking at the output of retesteth.
58+
59+
To get more insight about what is happening, you can use `--verbosity 6`. It will print every RPC call
60+
made by retesteth and also print out the state by using our `debug_*` endpoints. Note however that
61+
`debug_accountRange` and `debug_storageRangeAt` implementations are not complete at the moment :
62+
63+
- `debug_accountRange` will only list accounts known at the genesis state.
64+
- `debug_storageRangeAt` is not able to show the state after an arbitrary transaction inside a block.
65+
It will just return the state after all transaction in the block have run.

src/main/scala/io/iohk/ethereum/Mantis.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package io.iohk.ethereum
22

33
import io.iohk.ethereum.nodebuilder.{StdNode, TestNode}
44
import io.iohk.ethereum.utils.{Config, Logger}
5+
import org.rocksdb
6+
7+
import java.nio.file.{Files, Paths}
58
import java.util.logging.LogManager
69

710
object Mantis extends Logger {
@@ -11,6 +14,7 @@ object Mantis extends Logger {
1114
val node =
1215
if (Config.testmode) {
1316
log.info("Starting Mantis in test mode")
17+
deleteRocksDBFiles()
1418
new TestNode
1519
} else new StdNode
1620

@@ -19,4 +23,9 @@ object Mantis extends Logger {
1923

2024
node.start()
2125
}
26+
27+
private def deleteRocksDBFiles(): Unit = {
28+
log.warn("Deleting previous database {}", Config.Db.RocksDb.path)
29+
rocksdb.RocksDB.destroyDB(Config.Db.RocksDb.path, new rocksdb.Options())
30+
}
2231
}

src/main/scala/io/iohk/ethereum/domain/Blockchain.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,17 @@ class BlockchainImpl(
320320
val mpt =
321321
if (ethCompatibleStorage) domain.EthereumUInt256Mpt.storageMpt(rootHash, storage)
322322
else domain.ArbitraryIntegerMpt.storageMpt(rootHash, storage)
323-
ByteString(mpt.get(position).getOrElse(BigInt(0)).toByteArray)
323+
324+
val bigIntValue = mpt.get(position).getOrElse(BigInt(0))
325+
val byteArrayValue = bigIntValue.toByteArray
326+
327+
// BigInt.toArray actually might return one more byte than necessary because it adds a sign bit, which in our case
328+
// will always be 0. This would add unwanted 0 bytes and might cause the value to be 33 byte long while an EVM
329+
// word is 32 byte long.
330+
if (bigIntValue != 0)
331+
ByteString(byteArrayValue.dropWhile(_ == 0))
332+
else
333+
ByteString(byteArrayValue)
324334
}
325335

326336
override def getStorageProofAt(

src/main/scala/io/iohk/ethereum/jsonrpc/TestJsonMethodsImplicits.scala

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.iohk.ethereum.blockchain.data.GenesisAccount
1111

1212
import scala.util.Try
1313
import io.iohk.ethereum.domain.UInt256
14+
import io.iohk.ethereum.testmode.SealEngineType
1415
import org.json4s.Extraction
1516

1617
object TestJsonMethodsImplicits extends JsonMethodsImplicits {
@@ -58,12 +59,20 @@ object TestJsonMethodsImplicits extends JsonMethodsImplicits {
5859
for {
5960
genesis <- extractGenesis(paramsObj \ "genesis")
6061
blockchainParams <- extractBlockchainParams(paramsObj \ "params")
61-
sealEngine <- Try((paramsObj \ "sealEngine").extract[String]).toEither.leftMap(_ => InvalidParams())
62+
sealEngine <- Try((paramsObj \ "sealEngine").extract[String]).toEither
63+
.leftMap(_ => InvalidParams())
64+
.flatMap(extractSealEngine)
6265
accounts <- extractAccounts(paramsObj \ "accounts")
6366
} yield SetChainParamsRequest(ChainParams(genesis, blockchainParams, sealEngine, accounts))
6467
case _ => Left(InvalidParams())
6568
}
6669

70+
private def extractSealEngine(str: String) = str match {
71+
case "NoReward" => Right(SealEngineType.NoReward)
72+
case "NoProof" => Right(SealEngineType.NoProof)
73+
case other => Left(InvalidParams(s"unknown seal engine $other"))
74+
}
75+
6776
private def extractGenesis(genesisJson: JValue): Either[JsonRpcError, GenesisParams] = {
6877
for {
6978
author <- extractBytes((genesisJson \ "author").extract[String])
@@ -177,7 +186,7 @@ object TestJsonMethodsImplicits extends JsonMethodsImplicits {
177186
addressHash <- extractBytes(addressHash.extract[String])
178187
blockHashOrNumberEither = extractBlockHashOrNumber(blockHashOrNumber.extract[String])
179188
} yield AccountsInRangeRequest(
180-
AccountsInRangeRequestParams(blockHashOrNumberEither, txIndex, addressHash, maxResults)
189+
AccountsInRangeRequestParams(blockHashOrNumberEither, txIndex, addressHash, maxResults.toInt)
181190
)
182191
case _ => Left(InvalidParams())
183192
}
@@ -205,7 +214,7 @@ object TestJsonMethodsImplicits extends JsonMethodsImplicits {
205214
addressHash <- extractBytes(address.extract[String])
206215
blockHashOrNumberEither = extractBlockHashOrNumber(blockHashOrNumber.extract[String])
207216
} yield StorageRangeRequest(
208-
StorageRangeParams(blockHashOrNumberEither, txIndex, addressHash, begin, maxResults)
217+
StorageRangeParams(blockHashOrNumberEither, txIndex, addressHash, begin, maxResults.toInt)
209218
)
210219
case _ => Left(InvalidParams())
211220
}

src/main/scala/io/iohk/ethereum/jsonrpc/TestService.scala

Lines changed: 100 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import io.iohk.ethereum.{crypto, domain, rlp}
1010
import io.iohk.ethereum.domain.Block._
1111
import io.iohk.ethereum.domain.{Account, Address, Block, BlockchainImpl, UInt256}
1212
import io.iohk.ethereum.ledger._
13-
import io.iohk.ethereum.testmode.{TestModeComponentsProvider, TestmodeConsensus}
13+
import io.iohk.ethereum.testmode.{SealEngineType, TestModeComponentsProvider}
1414
import io.iohk.ethereum.transactions.PendingTransactionsManager
1515
import io.iohk.ethereum.transactions.PendingTransactionsManager.PendingTransactionsResponse
1616
import io.iohk.ethereum.utils.{BlockchainConfig, ByteStringUtils, ForkBlockNumbers, Logger}
@@ -50,15 +50,15 @@ object TestService {
5050
case class ChainParams(
5151
genesis: GenesisParams,
5252
blockchainParams: BlockchainParams,
53-
sealEngine: String,
53+
sealEngine: SealEngineType,
5454
accounts: Map[ByteString, GenesisAccount]
5555
)
5656

5757
case class AccountsInRangeRequestParams(
5858
blockHashOrNumber: Either[BigInt, ByteString],
5959
txIndex: BigInt,
6060
addressHash: ByteString,
61-
maxResults: BigInt
61+
maxResults: Int
6262
)
6363

6464
case class AccountsInRange(
@@ -71,7 +71,7 @@ object TestService {
7171
txIndex: BigInt,
7272
address: ByteString,
7373
begin: BigInt,
74-
maxResults: BigInt
74+
maxResults: Int
7575
)
7676

7777
case class StorageEntry(key: String, value: String)
@@ -98,7 +98,11 @@ object TestService {
9898
case class AccountsInRangeResponse(addressMap: Map[ByteString, ByteString], nextKey: ByteString)
9999

100100
case class StorageRangeRequest(parameters: StorageRangeParams)
101-
case class StorageRangeResponse(complete: Boolean, storage: Map[String, StorageEntry])
101+
case class StorageRangeResponse(
102+
complete: Boolean,
103+
storage: Map[String, StorageEntry],
104+
nextKey: Option[String]
105+
)
102106

103107
case class GetLogHashRequest(transactionHash: ByteString)
104108
case class GetLogHashResponse(logHash: ByteString)
@@ -109,7 +113,8 @@ class TestService(
109113
pendingTransactionsManager: ActorRef,
110114
consensusConfig: ConsensusConfig,
111115
testModeComponentsProvider: TestModeComponentsProvider,
112-
initialConfig: BlockchainConfig
116+
initialConfig: BlockchainConfig,
117+
preimageCache: collection.concurrent.Map[ByteString, UInt256]
113118
)(implicit
114119
scheduler: Scheduler
115120
) extends Logger {
@@ -118,10 +123,10 @@ class TestService(
118123
import io.iohk.ethereum.jsonrpc.AkkaTaskOps._
119124

120125
private var etherbase: Address = consensusConfig.coinbase
121-
private var accountAddresses: List[String] = List()
122-
private var accountRangeOffset = 0
126+
private var accountHashWithAdresses: List[(ByteString, Address)] = List()
123127
private var currentConfig: BlockchainConfig = initialConfig
124128
private var blockTimestamp: Long = 0
129+
private var sealEngine: SealEngineType = SealEngineType.NoReward
125130

126131
def setChainParams(request: SetChainParamsRequest): ServiceResponse[SetChainParamsResponse] = {
127132
currentConfig = buildNewConfig(request.chainParams.blockchainParams)
@@ -142,6 +147,10 @@ class TestService(
142147
// set coinbase for blocks that will be tried to mine
143148
etherbase = Address(genesisData.coinbase)
144149

150+
sealEngine = request.chainParams.sealEngine
151+
152+
resetPreimages(genesisData)
153+
145154
// remove current genesis (Try because it may not exist)
146155
Try(blockchain.removeBlock(blockchain.genesisHeader.hash, withState = false))
147156

@@ -153,8 +162,12 @@ class TestService(
153162
storeGenesisAccountCodes(genesisData.alloc)
154163
storeGenesisAccountStorageData(genesisData.alloc)
155164

156-
accountAddresses = genesisData.alloc.keys.toList
157-
accountRangeOffset = 0
165+
accountHashWithAdresses = (etherbase.toUnprefixedString :: genesisData.alloc.keys.toList)
166+
.map(hexAddress => {
167+
val address = Address(hexAddress)
168+
crypto.kec256(address.bytes) -> address
169+
})
170+
.sortBy(v => UInt256(v._1))
158171

159172
SetChainParamsResponse().rightNow
160173
}
@@ -214,7 +227,9 @@ class TestService(
214227
def mineBlocks(request: MineBlocksRequest): ServiceResponse[MineBlocksResponse] = {
215228
def mineBlock(): Task[Unit] = {
216229
getBlockForMining(blockchain.getBestBlock().get)
217-
.flatMap(blockForMining => testModeComponentsProvider.ledger(currentConfig).importBlock(blockForMining.block))
230+
.flatMap(blockForMining =>
231+
testModeComponentsProvider.ledger(currentConfig, sealEngine).importBlock(blockForMining.block)
232+
)
218233
.map { res =>
219234
log.info("Block mining result: " + res)
220235
pendingTransactionsManager ! PendingTransactionsManager.ClearPendingTransactions
@@ -245,10 +260,11 @@ class TestService(
245260

246261
def importRawBlock(request: ImportRawBlockRequest): ServiceResponse[ImportRawBlockResponse] = {
247262
Try(decode(request.blockRlp).toBlock) match {
248-
case Failure(_) => Task.now(Left(JsonRpcError(-1, "block validation failed!", None)))
263+
case Failure(_) =>
264+
Task.now(Left(JsonRpcError(-1, "block validation failed!", None)))
249265
case Success(value) =>
250266
testModeComponentsProvider
251-
.ledger(currentConfig)
267+
.ledger(currentConfig, sealEngine)
252268
.importBlock(value)
253269
.flatMap(handleResult)
254270
}
@@ -259,7 +275,9 @@ class TestService(
259275
case BlockImportedToTop(blockImportData) =>
260276
val blockHash = s"0x${ByteStringUtils.hash2string(blockImportData.head.block.header.hash)}"
261277
ImportRawBlockResponse(blockHash).rightNow
262-
case _ => Task.now(Left(JsonRpcError(-1, "block validation failed!", None)))
278+
case e =>
279+
log.warn("Block import failed with {}", e)
280+
Task.now(Left(JsonRpcError(-1, "block validation failed!", None)))
263281
}
264282
}
265283

@@ -268,6 +286,17 @@ class TestService(
268286
SetEtherbaseResponse().rightNow
269287
}
270288

289+
private def resetPreimages(genesisData: GenesisData): Unit = {
290+
preimageCache.clear()
291+
for {
292+
(_, account) <- genesisData.alloc
293+
storage <- account.storage
294+
storageKey <- storage.keys
295+
} {
296+
preimageCache.put(crypto.kec256(storageKey.bytes), storageKey)
297+
}
298+
}
299+
271300
private def getBlockForMining(parentBlock: Block): Task[PendingBlock] = {
272301
implicit val timeout: Timeout = Timeout(20.seconds)
273302
pendingTransactionsManager
@@ -276,7 +305,7 @@ class TestService(
276305
.onErrorRecover { case _ => PendingTransactionsResponse(Nil) }
277306
.map { pendingTxs =>
278307
testModeComponentsProvider
279-
.consensus(currentConfig, blockTimestamp)
308+
.consensus(currentConfig, sealEngine, blockTimestamp)
280309
.blockGenerator
281310
.generateBlock(
282311
parentBlock,
@@ -290,57 +319,87 @@ class TestService(
290319
.timeout(timeout.duration)
291320
}
292321

322+
/** Get the list of accounts of size _maxResults in the given _blockHashOrNumber after given _txIndex.
323+
* In response AddressMap contains addressHash - > address starting from given _addressHash.
324+
* nexKey field is the next addressHash (if any addresses left in the state).
325+
* @see https://github.com/ethereum/retesteth/wiki/RPC-Methods#debug_accountrange
326+
*/
293327
def getAccountsInRange(request: AccountsInRangeRequest): ServiceResponse[AccountsInRangeResponse] = {
328+
// This implementation works by keeping a list of know account from the genesis state
329+
// It might not cover all the cases as an account created inside a transaction won't be there.
330+
294331
val blockOpt = request.parameters.blockHashOrNumber
295332
.fold(number => blockchain.getBlockByNumber(number), blockHash => blockchain.getBlockByHash(blockHash))
296333

297334
if (blockOpt.isEmpty) {
298335
AccountsInRangeResponse(Map(), ByteString(0)).rightNow
299336
} else {
300-
val accountBatch = accountAddresses
301-
.slice(accountRangeOffset, accountRangeOffset + request.parameters.maxResults.toInt + 1)
337+
val accountBatch: Seq[(ByteString, Address)] = accountHashWithAdresses.view
338+
.dropWhile { case (hash, _) => UInt256(hash) < UInt256(request.parameters.addressHash) }
339+
.filter { case (_, address) => blockchain.getAccount(address, blockOpt.get.header.number).isDefined }
340+
.take(request.parameters.maxResults + 1)
341+
.to(Seq)
302342

303-
val addressesForExistingAccounts = accountBatch
304-
.filter(key => blockchain.getAccount(Address(key), blockOpt.get.header.number).isDefined)
305-
.map(key => (key, Address(crypto.kec256(Hex.decode(key)))))
343+
val addressMap: Map[ByteString, ByteString] = accountBatch
344+
.take(request.parameters.maxResults)
345+
.map { case (hash, address) => hash -> address.bytes }
346+
.to(Map)
306347

307348
AccountsInRangeResponse(
308-
addressMap = addressesForExistingAccounts
309-
.take(request.parameters.maxResults.toInt)
310-
.foldLeft(Map[ByteString, ByteString]())((el, addressPair) =>
311-
el + (addressPair._2.bytes -> ByteStringUtils.string2hash(addressPair._1))
312-
),
349+
addressMap = addressMap,
313350
nextKey =
314351
if (accountBatch.size > request.parameters.maxResults)
315-
ByteStringUtils.string2hash(addressesForExistingAccounts.last._1)
352+
accountBatch.last._1
316353
else UInt256(0).bytes
317354
).rightNow
318355
}
319356
}
320357

358+
/** Get the list of storage values starting from _begin and up to _begin + _maxResults at given block.
359+
* nexKey field is the next key hash if any key left in the state, or 0x00 otherwise.
360+
*
361+
* Normally, this RPC method is supposed to also be able to look up the state after after transaction
362+
* _txIndex is executed. This is currently not supported in mantis.
363+
* @see https://github.com/ethereum/retesteth/wiki/RPC-Methods#debug_storagerangeat
364+
*/
365+
// TODO ETCM-784, ETCM-758: see how we can get a state after an arbitrary transation
321366
def storageRangeAt(request: StorageRangeRequest): ServiceResponse[StorageRangeResponse] = {
322367

323368
val blockOpt = request.parameters.blockHashOrNumber
324369
.fold(number => blockchain.getBlockByNumber(number), hash => blockchain.getBlockByHash(hash))
325370

326371
(for {
327-
block <- blockOpt.toRight(StorageRangeResponse(complete = false, Map.empty))
372+
block <- blockOpt.toRight(StorageRangeResponse(complete = false, Map.empty, None))
328373
accountOpt = blockchain.getAccount(Address(request.parameters.address), block.header.number)
329-
account <- accountOpt.toRight(StorageRangeResponse(complete = false, Map.empty))
330-
storage = blockchain.getAccountStorageAt(
331-
account.storageRoot,
332-
request.parameters.begin,
333-
ethCompatibleStorage = true
334-
)
335-
} yield StorageRangeResponse(
336-
complete = true,
337-
storage = Map(
338-
encodeAsHex(request.parameters.address).values -> StorageEntry(
339-
encodeAsHex(request.parameters.begin).values,
340-
encodeAsHex(storage).values
341-
)
374+
account <- accountOpt.toRight(StorageRangeResponse(complete = false, Map.empty, None))
375+
376+
} yield {
377+
// This implementation might be improved. It is working for most tests in ETS but might be
378+
// not really efficient and would not work outside of a test context. We simply iterate over
379+
// every key known by the preimage cache.
380+
val (valueBatch, next) = preimageCache.toSeq
381+
.sortBy(v => UInt256(v._1))
382+
.view
383+
.dropWhile { case (hash, _) => UInt256(hash) < request.parameters.begin }
384+
.map { case (keyHash, keyValue) =>
385+
(keyHash.toArray, keyValue, blockchain.getAccountStorageAt(account.storageRoot, keyValue, true))
386+
}
387+
.filterNot { case (_, _, storageValue) => storageValue == ByteString(0) }
388+
.take(request.parameters.maxResults + 1)
389+
.splitAt(request.parameters.maxResults)
390+
391+
val storage = valueBatch
392+
.map { case (keyHash, keyValue, value) =>
393+
UInt256(keyHash).toHexString -> StorageEntry(keyValue.toHexString, UInt256(value).toHexString)
394+
}
395+
.to(Map)
396+
397+
StorageRangeResponse(
398+
complete = next.isEmpty,
399+
storage = storage,
400+
nextKey = next.headOption.map { case (hash, _, _) => UInt256(hash).toHexString }
342401
)
343-
)).fold(identity, identity).rightNow
402+
}).fold(identity, identity).rightNow
344403
}
345404

346405
def getLogHash(request: GetLogHashRequest): ServiceResponse[GetLogHashResponse] = {

0 commit comments

Comments
 (0)