[NC-1685] Improve tests for BlockHashOperation (#47)

* Introduce MessageFrameTestFixture to make it easier to test EVM operations.

* Add unit tests for BlockHashOperation.
Adrian Sutton 6 years ago committed by GitHub
parent b719a04a5c
commit 3e7a54850b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/TransactionProcessor.java
  2. 184
      ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/MessageFrameTestFixture.java
  3. 11
      ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/TestCodeExecutor.java
  4. 96
      ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/BlockHashOperationTest.java
  5. 29
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/DebugOperationTracerTest.java
  6. 43
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/TestBlockchain.java
  7. 25
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/operations/ExtCodeHashOperationTest.java

@ -92,7 +92,8 @@ public interface TransactionProcessor {
* @param worldState The current world state
* @param blockHeader The current block header
* @param transaction The transaction to process
* @param miningBeneficiary the address which is to receive the transaction fee
* @param miningBeneficiary The address which is to receive the transaction fee
* @param blockHashLookup The {@link BlockHashLookup} to use for BLOCKHASH operations
* @return the transaction result
*/
default Result processTransaction(
@ -120,7 +121,8 @@ public interface TransactionProcessor {
* @param blockHeader The current block header
* @param transaction The transaction to process
* @param operationTracer The tracer to record results of each EVM operation
* @param miningBeneficiary the address which is to receive the transaction fee
* @param miningBeneficiary The address which is to receive the transaction fee
* @param blockHashLookup The {@link BlockHashLookup} to use for BLOCKHASH operations
* @return the transaction result
*/
Result processTransaction(

@ -0,0 +1,184 @@
package net.consensys.pantheon.ethereum.core;
import tech.pegasys.pantheon.ethereum.chain.Blockchain;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.AddressHelpers;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.core.ExecutionContextTestFixture;
import tech.pegasys.pantheon.ethereum.core.Gas;
import tech.pegasys.pantheon.ethereum.core.MutableWorldState;
import tech.pegasys.pantheon.ethereum.core.Wei;
import tech.pegasys.pantheon.ethereum.core.WorldUpdater;
import tech.pegasys.pantheon.ethereum.vm.BlockHashLookup;
import tech.pegasys.pantheon.ethereum.vm.Code;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame.Type;
import tech.pegasys.pantheon.util.bytes.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
public class MessageFrameTestFixture {
private static final Address DEFAUT_ADDRESS = AddressHelpers.ofValue(244259721);
private Type type = Type.MESSAGE_CALL;
private Deque<MessageFrame> messageFrameStack = new ArrayDeque<>();
private Optional<Blockchain> blockchain = Optional.empty();
private Optional<WorldUpdater> worldState = Optional.empty();
private Gas initialGas = Gas.MAX_VALUE;
private Address address = DEFAUT_ADDRESS;
private Address sender = DEFAUT_ADDRESS;
private Address originator = DEFAUT_ADDRESS;
private Address contract = DEFAUT_ADDRESS;
private Wei gasPrice = Wei.ZERO;
private Wei value = Wei.ZERO;
private BytesValue inputData = BytesValue.EMPTY;
private Code code = new Code(BytesValue.EMPTY);
private final List<Bytes32> stackItems = new ArrayList<>();
private Optional<BlockHeader> blockHeader = Optional.empty();
private int depth = 0;
private Optional<BlockHashLookup> blockHashLookup = Optional.empty();
private ExecutionContextTestFixture executionContextTestFixture;
public MessageFrameTestFixture type(final Type type) {
this.type = type;
return this;
}
public MessageFrameTestFixture messageFrameStack(final Deque<MessageFrame> messageFrameStack) {
this.messageFrameStack = messageFrameStack;
return this;
}
public MessageFrameTestFixture blockchain(final Blockchain blockchain) {
this.blockchain = Optional.of(blockchain);
return this;
}
public MessageFrameTestFixture worldState(final WorldUpdater worldState) {
this.worldState = Optional.of(worldState);
return this;
}
public MessageFrameTestFixture worldState(final MutableWorldState worldState) {
this.worldState = Optional.of(worldState.updater());
return this;
}
public MessageFrameTestFixture initialGas(final Gas initialGas) {
this.initialGas = initialGas;
return this;
}
public MessageFrameTestFixture sender(final Address sender) {
this.sender = sender;
return this;
}
public MessageFrameTestFixture address(final Address address) {
this.address = address;
return this;
}
public MessageFrameTestFixture originator(final Address originator) {
this.originator = originator;
return this;
}
public MessageFrameTestFixture contract(final Address contract) {
this.contract = contract;
return this;
}
public MessageFrameTestFixture gasPrice(final Wei gasPrice) {
this.gasPrice = gasPrice;
return this;
}
public MessageFrameTestFixture value(final Wei value) {
this.value = value;
return this;
}
public MessageFrameTestFixture inputData(final BytesValue inputData) {
this.inputData = inputData;
return this;
}
public MessageFrameTestFixture code(final Code code) {
this.code = code;
return this;
}
public MessageFrameTestFixture blockHeader(final BlockHeader blockHeader) {
this.blockHeader = Optional.of(blockHeader);
return this;
}
public MessageFrameTestFixture depth(final int depth) {
this.depth = depth;
return this;
}
public MessageFrameTestFixture pushStackItem(final Bytes32 item) {
stackItems.add(item);
return this;
}
public MessageFrameTestFixture blockHashLookup(final BlockHashLookup blockHashLookup) {
this.blockHashLookup = Optional.of(blockHashLookup);
return this;
}
public MessageFrame build() {
final Blockchain blockchain = this.blockchain.orElseGet(this::createDefaultBlockchain);
final BlockHeader blockHeader =
this.blockHeader.orElseGet(() -> blockchain.getBlockHeader(0).get());
final MessageFrame frame =
MessageFrame.builder()
.type(type)
.messageFrameStack(messageFrameStack)
.blockchain(blockchain)
.worldState(worldState.orElseGet(this::createDefaultWorldState))
.initialGas(initialGas)
.address(address)
.originator(originator)
.gasPrice(gasPrice)
.inputData(inputData)
.sender(sender)
.value(value)
.apparentValue(value)
.contract(contract)
.code(code)
.blockHeader(blockHeader)
.depth(depth)
.completer(c -> {})
.miningBeneficiary(blockHeader.getCoinbase())
.blockHashLookup(
blockHashLookup.orElseGet(() -> new BlockHashLookup(blockHeader, blockchain)))
.build();
stackItems.forEach(frame::pushStackItem);
return frame;
}
private WorldUpdater createDefaultWorldState() {
return getOrCreateExecutionContextTestFixture().getStateArchive().getMutable().updater();
}
private Blockchain createDefaultBlockchain() {
return getOrCreateExecutionContextTestFixture().getBlockchain();
}
private ExecutionContextTestFixture getOrCreateExecutionContextTestFixture() {
// Avoid creating a test fixture if the test supplies the blockchain and worldstate.
if (executionContextTestFixture == null) {
executionContextTestFixture = new ExecutionContextTestFixture();
}
return executionContextTestFixture;
}
}

@ -6,10 +6,8 @@ import tech.pegasys.pantheon.ethereum.mainnet.MainnetMessageCallProcessor;
import tech.pegasys.pantheon.ethereum.mainnet.PrecompileContractRegistry;
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule;
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSpec;
import tech.pegasys.pantheon.ethereum.vm.BlockHashLookup;
import tech.pegasys.pantheon.ethereum.vm.Code;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame.Type;
import tech.pegasys.pantheon.ethereum.vm.OperationTracer;
import tech.pegasys.pantheon.util.bytes.BytesValue;
@ -18,6 +16,8 @@ import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Consumer;
import net.consensys.pantheon.ethereum.core.MessageFrameTestFixture;
public class TestCodeExecutor {
private final ExecutionContextTestFixture fixture;
@ -50,8 +50,7 @@ public class TestCodeExecutor {
.nonce(0)
.build();
final MessageFrame initialFrame =
MessageFrame.builder()
.type(Type.MESSAGE_CALL)
new MessageFrameTestFixture()
.messageFrameStack(messageFrameStack)
.blockchain(fixture.getBlockchain())
.worldState(worldState)
@ -63,13 +62,9 @@ public class TestCodeExecutor {
.inputData(transaction.getPayload())
.sender(SENDER_ADDRESS)
.value(transaction.getValue())
.apparentValue(transaction.getValue())
.code(new Code(BytesValue.fromHexString(code)))
.blockHeader(blockHeader)
.depth(0)
.completer(c -> {})
.miningBeneficiary(blockHeader.coinbase)
.blockHashLookup(new BlockHashLookup(blockHeader, fixture.getBlockchain()))
.build();
messageFrameStack.addFirst(initialFrame);

@ -0,0 +1,96 @@
package net.consensys.pantheon.ethereum.vm.operations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.mainnet.FrontierGasCalculator;
import tech.pegasys.pantheon.ethereum.vm.BlockHashLookup;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame;
import tech.pegasys.pantheon.ethereum.vm.operations.BlockHashOperation;
import tech.pegasys.pantheon.util.bytes.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.uint.UInt256;
import com.google.common.base.Strings;
import net.consensys.pantheon.ethereum.core.MessageFrameTestFixture;
import org.junit.After;
import org.junit.Test;
public class BlockHashOperationTest {
private static final int MAXIMUM_COMPLETE_BLOCKS_BEHIND = 256;
private final BlockHashLookup blockHashLookup = mock(BlockHashLookup.class);
private final BlockHashOperation blockHashOperation =
new BlockHashOperation(new FrontierGasCalculator());
@After
public void verifyNoUnexpectedHashLookups() {
verifyNoMoreInteractions(blockHashLookup);
}
@Test
public void shouldReturnZeroWhenArgIsBiggerThanALong() {
assertBlockHash(Bytes32.fromHexString(Strings.repeat("F", 64)), Bytes32.ZERO, 100);
}
@Test
public void shouldReturnZeroWhenCurrentBlockIsGenesis() {
assertBlockHash(Bytes32.ZERO, Bytes32.ZERO, BlockHeader.GENESIS_BLOCK_NUMBER);
}
@Test
public void shouldReturnZeroWhenRequestedBlockAheadOfCurrent() {
assertBlockHash(250, Bytes32.ZERO, 100);
}
@Test
public void shouldReturnZeroWhenRequestedBlockTooFarBehindCurrent() {
final int requestedBlock = 10;
// Our block is the one after the chain head (it's a new block), hence the + 1.
final int importingBlockNumber = MAXIMUM_COMPLETE_BLOCKS_BEHIND + requestedBlock + 1;
assertBlockHash(requestedBlock, Bytes32.ZERO, importingBlockNumber);
}
@Test
public void shouldReturnZeroWhenRequestedBlockGreaterThanImportingBlock() {
assertBlockHash(101, Bytes32.ZERO, 100);
}
@Test
public void shouldReturnZeroWhenRequestedBlockEqualToImportingBlock() {
assertBlockHash(100, Bytes32.ZERO, 100);
}
@Test
public void shouldReturnBlockHashUsingLookupFromFrameWhenItIsWithinTheAllowedRange() {
final Hash blockHash = Hash.hash(BytesValue.fromHexString("0x1293487297"));
when(blockHashLookup.getBlockHash(100)).thenReturn(blockHash);
assertBlockHash(100, blockHash, 200);
verify(blockHashLookup).getBlockHash(100);
}
private void assertBlockHash(
final long requestedBlock, final Bytes32 expectedOutput, final long currentBlockNumber) {
assertBlockHash(UInt256.of(requestedBlock).getBytes(), expectedOutput, currentBlockNumber);
}
private void assertBlockHash(
final Bytes32 input, final Bytes32 expectedOutput, final long currentBlockNumber) {
final MessageFrame frame =
new MessageFrameTestFixture()
.blockHashLookup(blockHashLookup)
.blockHeader(new BlockHeaderTestFixture().number(currentBlockNumber).buildHeader())
.pushStackItem(input)
.build();
blockHashOperation.execute(frame);
final Bytes32 result = frame.popStackItem();
assertThat(result).isEqualTo(expectedOutput);
assertThat(frame.stackSize()).isZero();
}
}

@ -5,11 +5,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static tech.pegasys.pantheon.ethereum.core.AddressHelpers.calculateAddressWithRespectTo;
import static tech.pegasys.pantheon.ethereum.vm.ExceptionalHaltReason.INSUFFICIENT_GAS;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.AddressHelpers;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture;
import tech.pegasys.pantheon.ethereum.core.Gas;
@ -21,15 +18,14 @@ import tech.pegasys.pantheon.ethereum.debug.TraceOptions;
import tech.pegasys.pantheon.ethereum.vm.OperationTracer.ExecuteOperation;
import tech.pegasys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException;
import tech.pegasys.pantheon.util.bytes.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.uint.UInt256;
import java.util.ArrayDeque;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import net.consensys.pantheon.ethereum.core.MessageFrameTestFixture;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@ -189,29 +185,16 @@ public class DebugOperationTracerTest {
return tracer.getTraceFrames().get(0);
}
private MessageFrame.Builder validMessageFrameBuilder() {
private MessageFrameTestFixture validMessageFrameBuilder() {
final BlockHeader blockHeader = new BlockHeaderTestFixture().number(1).buildHeader();
final TestBlockchain blockchain = new TestBlockchain(blockHeader.getNumber());
return MessageFrame.builder()
.type(MessageFrame.Type.MESSAGE_CALL)
.messageFrameStack(new ArrayDeque<>())
.blockchain(blockchain)
.worldState(worldUpdater)
return new MessageFrameTestFixture()
.initialGas(INITIAL_GAS)
.contract(calculateAddressWithRespectTo(Address.ID, 1))
.address(calculateAddressWithRespectTo(Address.ID, 2))
.originator(calculateAddressWithRespectTo(Address.ID, 3))
.worldState(worldUpdater)
.gasPrice(Wei.of(25))
.inputData(BytesValue.EMPTY)
.sender(calculateAddressWithRespectTo(Address.ID, 4))
.value(Wei.of(30))
.apparentValue(Wei.of(35))
.code(new Code())
.blockHeader(blockHeader)
.depth(DEPTH)
.completer(c -> {})
.miningBeneficiary(AddressHelpers.ofValue(0))
.blockHashLookup(new BlockHashLookup(blockHeader, blockchain));
.blockchain(blockchain)
.depth(DEPTH);
}
private Map<UInt256, UInt256> setupStorageForCapture(final MessageFrame frame) {

@ -23,14 +23,23 @@ import java.util.Optional;
/**
* A blockchain mock for the Ethereum reference tests.
*
* <p>The only method this class is used for is {@link TestBlockchain#getBlockHashByNumber} The
* Ethereum reference tests for VM exection (VMTests) and transaction processing (GeneralStateTests)
* require a block's hash to be to be the hash of the string of it's block number.
* <p>Operations which would lead to non-deterministic behaviour if executed while processing
* transactions throw {@link NonDeterministicOperationException}. For example all methods that
* lookup blocks by number since the block being processed may not be on the canonical chain but
* that must not affect the execution of its transactions.
*
* <p>The Ethereum reference tests for VM exection (VMTests) and transaction processing
* (GeneralStateTests) require a block's hash to be to be the hash of the string of it's block
* number.
*/
public class TestBlockchain implements Blockchain {
// Maximum number of blocks prior to the chain head that can be retrieved by hash.
private static final long MAXIMUM_BLOCKS_BEHIND_HEAD = 256;
private static final String NUMBER_LOOKUP_ERROR =
"Blocks must not be looked up by number in the EVM. The block being processed may not be on the canonical chain.";
private static final String CHAIN_HEAD_ERROR =
"Chain head is inherently non-deterministic. The block currently being processed should be treated as the chain head.";
private final Map<Hash, BlockHeader> hashToHeader = new HashMap<>();
public TestBlockchain() {
@ -58,32 +67,32 @@ public class TestBlockchain implements Blockchain {
@Override
public Optional<Hash> getBlockHashByNumber(final long number) {
return Optional.of(generateTestBlockHash(number));
throw new NonDeterministicOperationException(NUMBER_LOOKUP_ERROR);
}
@Override
public ChainHead getChainHead() {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException(CHAIN_HEAD_ERROR);
}
@Override
public long getChainHeadBlockNumber() {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException(CHAIN_HEAD_ERROR);
}
@Override
public Hash getChainHeadHash() {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException(CHAIN_HEAD_ERROR);
}
@Override
public Optional<TransactionLocation> getTransactionLocation(final Hash transactionHash) {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException("Transaction location may be different on forks");
}
@Override
public Optional<BlockHeader> getBlockHeader(final long blockNumber) {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException(NUMBER_LOOKUP_ERROR);
}
@Override
@ -93,31 +102,41 @@ public class TestBlockchain implements Blockchain {
@Override
public Optional<BlockBody> getBlockBody(final Hash blockHeaderHash) {
// Deterministic, but just not implemented.
throw new UnsupportedOperationException();
}
@Override
public Optional<List<TransactionReceipt>> getTxReceipts(final Hash blockHeaderHash) {
// Deterministic, but just not implemented.
throw new UnsupportedOperationException();
}
@Override
public Optional<UInt256> getTotalDifficultyByHash(final Hash blockHeaderHash) {
// Deterministic, but just not implemented.
throw new UnsupportedOperationException();
}
@Override
public Optional<Transaction> getTransactionByHash(final Hash transactionHash) {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException(
"Which transactions are on the chain may vary on different forks");
}
@Override
public long observeBlockAdded(final BlockAddedObserver observer) {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException("Listening for new blocks is not deterministic");
}
@Override
public boolean removeObserver(final long observerId) {
throw new UnsupportedOperationException();
throw new NonDeterministicOperationException("Listening for new blocks is not deterministic");
}
public static class NonDeterministicOperationException extends RuntimeException {
public NonDeterministicOperationException(final String message) {
super(message);
}
}
}

@ -14,10 +14,7 @@ import tech.pegasys.pantheon.ethereum.core.Wei;
import tech.pegasys.pantheon.ethereum.core.WorldUpdater;
import tech.pegasys.pantheon.ethereum.db.WorldStateArchive;
import tech.pegasys.pantheon.ethereum.mainnet.ConstantinopleGasCalculator;
import tech.pegasys.pantheon.ethereum.vm.BlockHashLookup;
import tech.pegasys.pantheon.ethereum.vm.Code;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame;
import tech.pegasys.pantheon.ethereum.vm.MessageFrame.Type;
import tech.pegasys.pantheon.ethereum.vm.Words;
import tech.pegasys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage;
import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage;
@ -25,13 +22,11 @@ import tech.pegasys.pantheon.util.bytes.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.uint.UInt256;
import java.util.ArrayDeque;
import net.consensys.pantheon.ethereum.core.MessageFrameTestFixture;
import org.junit.Test;
public class ExtCodeHashOperationTest {
private static final Address ADDRESS1 = AddressHelpers.ofValue(11111111);
private static final Address REQUESTED_ADDRESS = AddressHelpers.ofValue(22222222);
private final Blockchain blockchain = mock(Blockchain.class);
@ -108,26 +103,10 @@ public class ExtCodeHashOperationTest {
private MessageFrame createMessageFrame(final Bytes32 stackItem) {
final BlockHeader blockHeader = new BlockHeaderTestFixture().buildHeader();
final MessageFrame frame =
new MessageFrame.Builder()
.type(Type.MESSAGE_CALL)
.initialGas(Gas.MAX_VALUE)
.inputData(BytesValue.EMPTY)
.depth(1)
.gasPrice(Wei.ZERO)
.contract(ADDRESS1)
.address(ADDRESS1)
.originator(ADDRESS1)
.sender(ADDRESS1)
new MessageFrameTestFixture()
.worldState(worldStateUpdater)
.messageFrameStack(new ArrayDeque<>())
.blockHeader(blockHeader)
.value(Wei.ZERO)
.apparentValue(Wei.ZERO)
.code(new Code(BytesValue.EMPTY))
.blockchain(blockchain)
.completer(messageFrame -> {})
.miningBeneficiary(AddressHelpers.ofValue(0))
.blockHashLookup(new BlockHashLookup(blockHeader, blockchain))
.build();
frame.pushStackItem(stackItem);

Loading…
Cancel
Save