This is an automated email from the ASF dual-hosted git repository. toulmean pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git
The following commit(s) were added to refs/heads/main by this push: new 2479c3a add block processor lib new b6daad7 Merge pull request #383 from atoulme/block_processor 2479c3a is described below commit 2479c3abe01e58fa80da87e86aec8ffcbf04c926 Author: Antoine Toulme <anto...@lunar-ocean.com> AuthorDate: Wed Mar 16 22:24:01 2022 -0700 add block processor lib --- eth-blockprocessor/build.gradle | 52 +++++++ .../apache/tuweni/blockprocessor/BlockProcessor.kt | 159 +++++++++++++++++++++ .../org/apache/tuweni/blockprocessor/ProtoBlock.kt | 114 +++++++++++++++ .../tuweni/blockprocessor/BlockProcessorTest.kt | 46 ++++++ settings.gradle | 3 +- 5 files changed, 373 insertions(+), 1 deletion(-) diff --git a/eth-blockprocessor/build.gradle b/eth-blockprocessor/build.gradle new file mode 100644 index 0000000..c656367 --- /dev/null +++ b/eth-blockprocessor/build.gradle @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE + * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +description = 'Ethereum Block Processor' + +dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'dnsjava:dnsjava' + implementation 'io.opentelemetry:opentelemetry-api' + implementation 'io.opentelemetry:opentelemetry-sdk' + implementation 'io.opentelemetry:opentelemetry-sdk-metrics' + implementation 'io.vertx:vertx-core' + implementation 'org.fusesource.leveldbjni:leveldbjni-all' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + implementation 'org.apache.lucene:lucene-core' + implementation 'org.infinispan:infinispan-core' + implementation 'org.infinispan:infinispan-cachestore-rocksdb' + + implementation project(':bytes') + implementation project(':concurrent-coroutines') + implementation project(':crypto') + implementation project(':eth') + implementation project(':genesis') + implementation project(':eth-repository') + implementation project(':evm') + implementation project(':kv') + implementation project(':metrics') + implementation project(':rlp') + implementation project(':units') + implementation project(':merkle-trie') + + testImplementation project(':junit') + testImplementation 'org.bouncycastle:bcprov-jdk15on' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin' + testImplementation 'org.mockito:mockito-junit-jupiter' + testImplementation 'ch.qos.logback:logback-classic' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + + runtimeOnly 'ch.qos.logback:logback-classic' +} diff --git a/eth-blockprocessor/src/main/kotlin/org/apache/tuweni/blockprocessor/BlockProcessor.kt b/eth-blockprocessor/src/main/kotlin/org/apache/tuweni/blockprocessor/BlockProcessor.kt new file mode 100644 index 0000000..baf537a --- /dev/null +++ b/eth-blockprocessor/src/main/kotlin/org/apache/tuweni/blockprocessor/BlockProcessor.kt @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tuweni.blockprocessor + +import org.apache.tuweni.eth.AccountState +import org.apache.tuweni.eth.Address +import org.apache.tuweni.eth.Block +import org.apache.tuweni.eth.Hash +import org.apache.tuweni.eth.LogsBloomFilter +import org.apache.tuweni.eth.Transaction +import org.apache.tuweni.eth.TransactionReceipt +import org.apache.tuweni.eth.repository.BlockchainRepository +import org.apache.tuweni.evm.EVMExecutionStatusCode +import org.apache.tuweni.evm.EthereumVirtualMachine +import org.apache.tuweni.evm.impl.EvmVmImpl +import org.apache.tuweni.rlp.RLP +import org.apache.tuweni.trie.MerklePatriciaTrie +import org.apache.tuweni.trie.MerkleTrie +import org.apache.tuweni.units.bigints.UInt256 +import org.apache.tuweni.units.ethereum.Gas +import org.apache.tuweni.units.ethereum.Wei +import java.time.Instant + +/** + * A block processor executing blocks, executing in sequence transactions + * and committing data to a blockchain repository. + */ +class BlockProcessor { + + suspend fun execute(parentBlock: Block, transactions: List<Transaction>, repository: BlockchainRepository): ProtoBlock { + val vm = EthereumVirtualMachine(repository, EvmVmImpl::create) + vm.start() + var index = 0L + + val bloomFilter = LogsBloomFilter() + + val transactionsTrie = MerklePatriciaTrie.storingBytes() + val receiptsTrie = MerklePatriciaTrie.storingBytes() + val allReceipts = mutableListOf<TransactionReceipt>() + + var counter = 0L + var allGasUsed = Gas.ZERO + for (tx in transactions) { + val indexKey = RLP.encodeValue(UInt256.valueOf(counter).trimLeadingZeros()) + transactionsTrie.put(indexKey, tx.toBytes()) + if (null == tx.to) { + val contractAddress = Address.fromBytes( + Hash.hash( + RLP.encodeList { + it.writeValue(tx.sender!!) + it.writeValue(tx.nonce) + } + ).slice(12) + ) + val state = AccountState( + UInt256.ONE, + Wei.valueOf(0), + Hash.fromBytes(MerkleTrie.EMPTY_TRIE_ROOT_HASH), + org.apache.tuweni.eth.Hash.hash(tx.payload) + ) + repository.storeAccount(contractAddress, state) + repository.storeCode(tx.payload) + val receipt = TransactionReceipt( + 1, + 0, // TODO + LogsBloomFilter(), + emptyList() + ) + allReceipts.add(receipt) + receiptsTrie.put(indexKey, receipt.toBytes()) + counter++ + } else { + val code = repository.getAccountCode(tx.to!!) + val result = vm.execute( + tx.sender!!, + tx.to!!, + tx.value, + code!!, + tx.payload, + parentBlock.header.gasLimit, + tx.gasPrice, + Address.ZERO, + index, + Instant.now().toEpochMilli(), + tx.gasLimit.toLong(), + parentBlock.header.difficulty + ) + if (result.statusCode != EVMExecutionStatusCode.SUCCESS) { + throw Exception("invalid transaction result") + } + for (balanceChange in result.changes.getBalanceChanges()) { + val state = repository.getAccount(balanceChange.key)?.let { + AccountState(it.nonce, balanceChange.value, it.storageRoot, it.codeHash) + } ?: repository.newAccountState() + repository.storeAccount(balanceChange.key, state) + } + + for (storageChange in result.changes.getAccountChanges()) { + for (oneStorageChange in storageChange.value) { + repository.storeAccountValue(storageChange.key, oneStorageChange.key, oneStorageChange.value) + } + } + + for (accountToDestroy in result.changes.accountsToDestroy()) { + repository.destroyAccount(accountToDestroy) + } + for (log in result.changes.getLogs()) { + bloomFilter.insertLog(log) + } + + val txLogsBloomFilter = LogsBloomFilter() + for (log in result.changes.getLogs()) { + bloomFilter.insertLog(log) + } + val receipt = TransactionReceipt( + 1, + result.gasManager.gasCost.toLong(), + txLogsBloomFilter, + result.changes.getLogs() + ) + allReceipts.add(receipt) + receiptsTrie.put(indexKey, receipt.toBytes()) + counter++ + + allGasUsed = allGasUsed.add(result.gasManager.gasCost) + } + } + + val block = ProtoBlock( + SealableHeader( + parentBlock.header.hash, + Hash.fromBytes(repository.worldState!!.rootHash()), + Hash.fromBytes(transactionsTrie.rootHash()), + Hash.fromBytes(receiptsTrie.rootHash()), + bloomFilter.toBytes(), + parentBlock.header.number.add(1), + parentBlock.header.gasLimit, + allGasUsed, + ), + ProtoBlockBody(transactions), + allReceipts + ) + return block + } +} diff --git a/eth-blockprocessor/src/main/kotlin/org/apache/tuweni/blockprocessor/ProtoBlock.kt b/eth-blockprocessor/src/main/kotlin/org/apache/tuweni/blockprocessor/ProtoBlock.kt new file mode 100644 index 0000000..c747840 --- /dev/null +++ b/eth-blockprocessor/src/main/kotlin/org/apache/tuweni/blockprocessor/ProtoBlock.kt @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tuweni.blockprocessor + +import org.apache.tuweni.bytes.Bytes +import org.apache.tuweni.eth.Address +import org.apache.tuweni.eth.Block +import org.apache.tuweni.eth.BlockBody +import org.apache.tuweni.eth.BlockHeader +import org.apache.tuweni.eth.Hash +import org.apache.tuweni.eth.Transaction +import org.apache.tuweni.eth.TransactionReceipt +import org.apache.tuweni.rlp.RLP +import org.apache.tuweni.units.bigints.UInt256 +import org.apache.tuweni.units.bigints.UInt64 +import org.apache.tuweni.units.ethereum.Gas +import java.time.Instant + +/** + * A block header that has not finished being sealed. + */ +data class SealableHeader( + val parentHash: Hash, + val stateRoot: Hash, + val transactionsRoot: Hash, + val receiptsRoot: Hash, + val logsBloom: Bytes, + val number: UInt256, + val gasLimit: Gas, + val gasUsed: Gas, +) { + + /** + * Seals the header into a block header + */ + fun toHeader( + ommersHash: Hash, + coinbase: Address, + difficulty: UInt256, + timestamp: Instant, + extraData: Bytes, + mixHash: Hash, + nonce: UInt64, + ): BlockHeader { + return BlockHeader( + parentHash, + ommersHash, + coinbase, + stateRoot, + transactionsRoot, + receiptsRoot, + logsBloom, + difficulty, + number, + gasLimit, + gasUsed, + timestamp, + extraData, + mixHash, + nonce + ) + } +} + +/** + * A proto-block body is the representation of the intermediate form of a block body before being sealed. + */ +data class ProtoBlockBody(val transactions: List<Transaction>) { + /** + * Transforms the proto-block body into a valid block body by adding ommers. + */ + fun toBlockBody(ommers: List<BlockHeader>): BlockBody { + return BlockBody(transactions, ommers) + } +} + +/** + * A proto-block is a block that has been executed but has not been sealed. + * The header is missing the nonce and mixhash, and can still accept extra data. + * + * Proto-blocks are produced when transactions are executed, and can be turned into full valid blocks. + */ +class ProtoBlock(val header: SealableHeader, val body: ProtoBlockBody, val transactionReceipts: List<TransactionReceipt>) { + + fun toBlock( + ommers: List<BlockHeader>, + coinbase: Address, + difficulty: UInt256, + timestamp: Instant, + extraData: Bytes, + mixHash: Hash, + nonce: UInt64, + ): Block { + val ommersHash = Hash.hash(RLP.encodeList { writer -> ommers.forEach { writer.writeValue(it.hash) } }) + return Block( + header.toHeader(ommersHash, coinbase, difficulty, timestamp, extraData, mixHash, nonce), + body.toBlockBody(ommers) + ) + } +} diff --git a/eth-blockprocessor/src/test/kotlin/org/apache/tuweni/blockprocessor/BlockProcessorTest.kt b/eth-blockprocessor/src/test/kotlin/org/apache/tuweni/blockprocessor/BlockProcessorTest.kt new file mode 100644 index 0000000..b132836 --- /dev/null +++ b/eth-blockprocessor/src/test/kotlin/org/apache/tuweni/blockprocessor/BlockProcessorTest.kt @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tuweni.blockprocessor + +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.bytes.Bytes +import org.apache.tuweni.eth.Address +import org.apache.tuweni.eth.repository.BlockchainRepository +import org.apache.tuweni.genesis.Genesis +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.units.bigints.UInt256 +import org.apache.tuweni.units.bigints.UInt64 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.Instant + +@ExtendWith(BouncyCastleExtension::class) +class BlockProcessorTest { + + /** + * Demonstrate how the proto block can be created. + */ + @Test + fun testValidBlockNoTransactions() = runBlocking { + val processor = BlockProcessor() + val repository = BlockchainRepository.inMemory(Genesis.dev()) + val protoBlock = processor.execute(Genesis.dev(), listOf(), repository) + val block = protoBlock.toBlock(listOf(), Address.ZERO, UInt256.ONE, Instant.now(), Bytes.EMPTY, Genesis.emptyHash, UInt64.random()) + assertEquals(0, block.body.transactions.size) + } +} diff --git a/settings.gradle b/settings.gradle index b798cc2..7c27b1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,16 +23,17 @@ include 'devp2p-proxy' include 'dist' include 'dns-discovery' include 'eth' +include 'eth-blockprocessor' include 'eth-client' include 'eth-client-app' include 'eth-client-ui' include 'eth-crawler' +include 'eth-faucet' include 'eth2-reference-tests' include 'eth-reference-tests' include 'eth-repository' include 'ethstats' include 'evm' -include 'eth-faucet' include 'genesis' include 'gossip' include 'hobbits' --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@tuweni.apache.org For additional commands, e-mail: commits-h...@tuweni.apache.org