Skip to content

Commit

Permalink
Implement eth_getBlockByNumber while downloading state. (#2481)
Browse files Browse the repository at this point in the history
Introduces a dependency from eth_getBlockByNumber onto the Synchronizer, in order to determine download state, and current most recent block that we have state for. JsonRPC test classes also were refactored to provide a Synchronizer.
Signed-off-by: Justin Florentine <[email protected]>
  • Loading branch information
jflo authored Jun 29, 2021
1 parent 69223b8 commit 55cde86
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,67 @@ public class JsonRpcTestMethodsFactory {
private static final BigInteger NETWORK_ID = BigInteger.valueOf(123);

private final BlockchainImporter importer;
private final MutableBlockchain blockchain;
private final WorldStateArchive stateArchive;
private final ProtocolContext context;
private final BlockchainQueries blockchainQueries;
private final Synchronizer synchronizer;

public JsonRpcTestMethodsFactory(final BlockchainImporter importer) {
this.importer = importer;
}

public Map<String, JsonRpcMethod> methods() {
final WorldStateArchive stateArchive = createInMemoryWorldStateArchive();

importer.getGenesisState().writeStateTo(stateArchive.getMutable());

final MutableBlockchain blockchain = createInMemoryBlockchain(importer.getGenesisBlock());
final ProtocolContext context = new ProtocolContext(blockchain, stateArchive, null);
this.blockchain = createInMemoryBlockchain(importer.getGenesisBlock());
this.stateArchive = createInMemoryWorldStateArchive();
this.importer.getGenesisState().writeStateTo(stateArchive.getMutable());
this.context = new ProtocolContext(blockchain, stateArchive, null);

final ProtocolSchedule protocolSchedule = importer.getProtocolSchedule();
this.synchronizer = mock(Synchronizer.class);

for (final Block block : importer.getBlocks()) {
final ProtocolSpec protocolSpec =
protocolSchedule.getByBlockNumber(block.getHeader().getNumber());
final BlockImporter blockImporter = protocolSpec.getBlockImporter();
blockImporter.importBlock(context, block, HeaderValidationMode.FULL);
}
this.blockchainQueries = new BlockchainQueries(blockchain, stateArchive);
}

final BlockchainQueries blockchainQueries = new BlockchainQueries(blockchain, stateArchive);
public JsonRpcTestMethodsFactory(
final BlockchainImporter importer,
final MutableBlockchain blockchain,
final WorldStateArchive stateArchive,
final ProtocolContext context) {
this.importer = importer;
this.blockchain = blockchain;
this.stateArchive = stateArchive;
this.context = context;
this.blockchainQueries = new BlockchainQueries(blockchain, stateArchive);
this.synchronizer = mock(Synchronizer.class);
}

final Synchronizer synchronizer = mock(Synchronizer.class);
public JsonRpcTestMethodsFactory(
final BlockchainImporter importer,
final MutableBlockchain blockchain,
final WorldStateArchive stateArchive,
final ProtocolContext context,
final Synchronizer synchronizer) {
this.importer = importer;
this.blockchain = blockchain;
this.stateArchive = stateArchive;
this.context = context;
this.synchronizer = synchronizer;
this.blockchainQueries = new BlockchainQueries(blockchain, stateArchive);
}

public BlockchainQueries getBlockchainQueries() {
return blockchainQueries;
}

public WorldStateArchive getStateArchive() {
return stateArchive;
}

public Map<String, JsonRpcMethod> methods() {
final P2PNetwork peerDiscovery = mock(P2PNetwork.class);
final EthPeers ethPeers = mock(EthPeers.class);
final TransactionPool transactionPool = mock(TransactionPool.class);
Expand Down Expand Up @@ -126,7 +163,7 @@ public Map<String, JsonRpcMethod> methods() {
peerDiscovery,
blockchainQueries,
synchronizer,
protocolSchedule,
importer.getProtocolSchedule(),
filterManager,
transactionPool,
miningCoordinator,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright ConsenSys AG.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.hyperledger.besu.ethereum.api.jsonrpc.methods;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.BlockchainImporter;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcTestMethodsFactory;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResult;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockImporter;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.plugin.data.SyncStatus;
import org.hyperledger.besu.testutil.BlockTestUtil;

import java.util.Optional;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import org.assertj.core.api.Assertions;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class EthGetBlockByNumberLatestDesyncIntegrationTest {

private static JsonRpcTestMethodsFactory methodsFactorySynced;
private static JsonRpcTestMethodsFactory methodsFactoryDesynced;
private static JsonRpcTestMethodsFactory methodsFactoryMidDownload;
private static final long ARBITRARY_SYNC_BLOCK = 4L;

@BeforeClass
public static void setUpOnce() throws Exception {
final String genesisJson =
Resources.toString(BlockTestUtil.getTestGenesisUrl(), Charsets.UTF_8);
BlockchainImporter importer =
new BlockchainImporter(BlockTestUtil.getTestBlockchainUrl(), genesisJson);
MutableBlockchain chain =
InMemoryKeyValueStorageProvider.createInMemoryBlockchain(importer.getGenesisBlock());
WorldStateArchive state = InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive();
importer.getGenesisState().writeStateTo(state.getMutable());
ProtocolContext context = new ProtocolContext(chain, state, null);

for (final Block block : importer.getBlocks()) {
final ProtocolSchedule protocolSchedule = importer.getProtocolSchedule();
final ProtocolSpec protocolSpec =
protocolSchedule.getByBlockNumber(block.getHeader().getNumber());
final BlockImporter blockImporter = protocolSpec.getBlockImporter();
blockImporter.importBlock(context, block, HeaderValidationMode.FULL);
}

methodsFactorySynced = new JsonRpcTestMethodsFactory(importer, chain, state, context);

WorldStateArchive unsynced = mock(WorldStateArchive.class);
when(unsynced.isWorldStateAvailable(any(Hash.class), any(Hash.class))).thenReturn(false);

methodsFactoryDesynced = new JsonRpcTestMethodsFactory(importer, chain, unsynced, context);

WorldStateArchive midSync = mock(WorldStateArchive.class);
when(midSync.isWorldStateAvailable(any(Hash.class), any(Hash.class))).thenReturn(true);

Synchronizer synchronizer = mock(Synchronizer.class);
SyncStatus status = mock(SyncStatus.class);
when(status.getCurrentBlock())
.thenReturn(ARBITRARY_SYNC_BLOCK); // random choice for current sync state.
when(synchronizer.getSyncStatus()).thenReturn(Optional.of(status));

methodsFactoryMidDownload =
new JsonRpcTestMethodsFactory(importer, chain, midSync, context, synchronizer);
}

@Test
public void shouldReturnHeadIfFullySynced() {
JsonRpcMethod ethGetBlockNumber = methodsFactorySynced.methods().get("eth_getBlockByNumber");
Object[] params = {"latest", false};
JsonRpcRequestContext ctx =
new JsonRpcRequestContext(new JsonRpcRequest("2.0", "eth_getBlockByNumber", params));
Assertions.assertThatNoException()
.isThrownBy(
() -> {
final JsonRpcResponse resp = ethGetBlockNumber.response(ctx);
assertThat(resp).isNotNull();
assertThat(resp).isInstanceOf(JsonRpcSuccessResponse.class);
Object r = ((JsonRpcSuccessResponse) resp).getResult();
assertThat(r).isInstanceOf(BlockResult.class);
BlockResult br = (BlockResult) r;
assertThat(br.getNumber()).isEqualTo("0x20");
// assert on the state existing?
});
}

@Test
public void shouldReturnGenesisIfNotSynced() {

JsonRpcMethod ethGetBlockNumber = methodsFactoryDesynced.methods().get("eth_getBlockByNumber");
Object[] params = {"latest", false};
JsonRpcRequestContext ctx =
new JsonRpcRequestContext(new JsonRpcRequest("2.0", "eth_getBlockByNumber", params));
Assertions.assertThatNoException()
.isThrownBy(
() -> {
final JsonRpcResponse resp = ethGetBlockNumber.response(ctx);
assertThat(resp).isNotNull();
assertThat(resp).isInstanceOf(JsonRpcSuccessResponse.class);
Object r = ((JsonRpcSuccessResponse) resp).getResult();
assertThat(r).isInstanceOf(BlockResult.class);
BlockResult br = (BlockResult) r;
assertThat(br.getNumber()).isEqualTo("0x0");
});
}

@Test
public void shouldReturnCurrentSyncedIfDownloadingWorldState() {
JsonRpcMethod ethGetBlockNumber =
methodsFactoryMidDownload.methods().get("eth_getBlockByNumber");
Object[] params = {"latest", false};
JsonRpcRequestContext ctx =
new JsonRpcRequestContext(new JsonRpcRequest("2.0", "eth_getBlockByNumber", params));
Assertions.assertThatNoException()
.isThrownBy(
() -> {
final JsonRpcResponse resp = ethGetBlockNumber.response(ctx);
assertThat(resp).isNotNull();
assertThat(resp).isInstanceOf(JsonRpcSuccessResponse.class);
Object r = ((JsonRpcSuccessResponse) resp).getResult();
assertThat(r).isInstanceOf(BlockResult.class);
BlockResult br = (BlockResult) r;
assertThat(br.getNumber()).isEqualTo("0x4");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,39 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResultFactory;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.core.Synchronizer;

import java.util.function.Supplier;

import com.google.common.base.Suppliers;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class EthGetBlockByNumber extends AbstractBlockParameterMethod {

private final BlockResultFactory blockResult;
private final boolean includeCoinbase;
private static final Logger LOGGER = LogManager.getLogger();
private final Synchronizer synchronizer;

public EthGetBlockByNumber(
final BlockchainQueries blockchain, final BlockResultFactory blockResult) {
this(Suppliers.ofInstance(blockchain), blockResult, false);
final BlockchainQueries blockchain,
final BlockResultFactory blockResult,
final Synchronizer synchronizer) {
this(Suppliers.ofInstance(blockchain), blockResult, synchronizer, false);
}

public EthGetBlockByNumber(
final Supplier<BlockchainQueries> blockchain,
final BlockResultFactory blockResult,
final Synchronizer synchronizer,
final boolean includeCoinbase) {
super(blockchain);
this.blockResult = blockResult;
this.synchronizer = synchronizer;
this.includeCoinbase = includeCoinbase;
}

Expand All @@ -64,6 +76,30 @@ protected Object resultByBlockNumber(
return transactionHash(blockNumber);
}

@Override
protected Object latestResult(final JsonRpcRequestContext request) {

final long headBlockNumber = blockchainQueries.get().headBlockNumber();
Blockchain chain = blockchainQueries.get().getBlockchain();
BlockHeader headHeader = chain.getBlockHeader(headBlockNumber).orElse(null);

Hash block = headHeader.getHash();
Hash stateRoot = headHeader.getStateRoot();

if (blockchainQueries.get().getWorldStateArchive().isWorldStateAvailable(stateRoot, block)) {
if (this.synchronizer.getSyncStatus().isEmpty()) { // we are already in sync
return resultByBlockNumber(request, headBlockNumber);
} else { // out of sync, return highest pulled block
long headishBlock = this.synchronizer.getSyncStatus().get().getCurrentBlock();
return resultByBlockNumber(request, headishBlock);
}
}

LOGGER.trace("no world state available for block {} returning genesis", headBlockNumber);
return resultByBlockNumber(
request, blockchainQueries.get().getBlockchain().getGenesisBlock().getHeader().getNumber());
}

private BlockResult transactionComplete(final long blockNumber) {
return getBlockchainQueries()
.blockByNumber(blockNumber)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ protected Map<String, JsonRpcMethod> create() {
new EthBlockNumber(blockchainQueries),
new EthGetBalance(blockchainQueries),
new EthGetBlockByHash(blockchainQueries, blockResult),
new EthGetBlockByNumber(blockchainQueries, blockResult),
new EthGetBlockByNumber(blockchainQueries, blockResult, synchronizer),
new EthGetBlockTransactionCountByNumber(blockchainQueries),
new EthGetBlockTransactionCountByHash(blockchainQueries),
new EthCall(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.Wei;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.plugin.data.SyncStatus;

import java.math.BigInteger;
Expand Down Expand Up @@ -1057,6 +1058,12 @@ public void getBlockByNumberForLatest() throws Exception {
blockWithMetadata(block);
when(blockchainQueries.headBlockNumber()).thenReturn(0L);
when(blockchainQueries.blockByNumber(eq(0L))).thenReturn(Optional.of(blockWithMetadata));
when(blockchainQueries.getBlockchain()).thenReturn(blockchain);
when(blockchain.getBlockHeader(blockchainQueries.headBlockNumber()))
.thenReturn(Optional.of(block.getHeader()));
WorldStateArchive state = mock(WorldStateArchive.class);
when(state.isWorldStateAvailable(any(Hash.class), any(Hash.class))).thenReturn(true);
when(blockchainQueries.getWorldStateArchive()).thenReturn(state);

try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
assertThat(resp.code()).isEqualTo(200);
Expand All @@ -1079,6 +1086,19 @@ public void getBlockByNumberForPending() throws Exception {
"{\"jsonrpc\":\"2.0\",\"id\":"
+ Json.encode(id)
+ ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"pending\",true]}");
// Setup mocks to return a block
final BlockDataGenerator gen = new BlockDataGenerator();
final Block block = gen.genesisBlock();
final BlockWithMetadata<TransactionWithMetadata, Hash> blockWithMetadata =
blockWithMetadata(block);
when(blockchainQueries.headBlockNumber()).thenReturn(0L);
when(blockchainQueries.blockByNumber(eq(0L))).thenReturn(Optional.of(blockWithMetadata));
when(blockchainQueries.getBlockchain()).thenReturn(blockchain);
when(blockchain.getBlockHeader(blockchainQueries.headBlockNumber()))
.thenReturn(Optional.of(block.getHeader()));
WorldStateArchive state = mock(WorldStateArchive.class);
when(state.isWorldStateAvailable(any(Hash.class), any(Hash.class))).thenReturn(true);
when(blockchainQueries.getWorldStateArchive()).thenReturn(state);

try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
assertThat(resp.code()).isEqualTo(200);
Expand All @@ -1088,7 +1108,7 @@ public void getBlockByNumberForPending() throws Exception {
testHelper.assertValidJsonRpcResult(json, id);
// Check result
final JsonObject result = json.getJsonObject("result");
assertThat(result).isNull();
verifyBlockResult(block, blockWithMetadata.getTotalDifficulty(), result, false);
}
}

Expand Down
Loading

0 comments on commit 55cde86

Please sign in to comment.