|
|
|
@ -13,8 +13,11 @@ |
|
|
|
|
package tech.pegasys.pantheon.ethereum.eth.sync.state; |
|
|
|
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat; |
|
|
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
|
|
import static org.mockito.ArgumentMatchers.eq; |
|
|
|
|
import static org.mockito.Mockito.doReturn; |
|
|
|
|
import static org.mockito.Mockito.mock; |
|
|
|
|
import static org.mockito.Mockito.spy; |
|
|
|
|
import static org.mockito.Mockito.verify; |
|
|
|
|
import static org.mockito.Mockito.verifyNoMoreInteractions; |
|
|
|
|
import static org.mockito.Mockito.when; |
|
|
|
@ -22,16 +25,20 @@ import static org.mockito.Mockito.when; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.chain.BlockAddedEvent; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.chain.BlockAddedObserver; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.chain.Blockchain; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.chain.ChainHead; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.core.Block; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.core.BlockBody; |
|
|
|
|
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.core.Synchronizer.SyncStatusListener; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.eth.manager.ChainState; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
|
|
|
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeers; |
|
|
|
|
import tech.pegasys.pantheon.util.uint.UInt256; |
|
|
|
|
|
|
|
|
|
import java.util.Collections; |
|
|
|
|
import java.util.Optional; |
|
|
|
|
|
|
|
|
|
import org.junit.Before; |
|
|
|
|
import org.junit.Test; |
|
|
|
@ -40,12 +47,18 @@ import org.mockito.ArgumentCaptor; |
|
|
|
|
public class SyncStateTest { |
|
|
|
|
|
|
|
|
|
private static final long OUR_CHAIN_HEAD_NUMBER = 500; |
|
|
|
|
private static final long TARGET_SYNC_NUMBER = OUR_CHAIN_HEAD_NUMBER + 100; |
|
|
|
|
private static final UInt256 OUR_CHAIN_DIFFICULTY = UInt256.of(500); |
|
|
|
|
private static final long TARGET_CHAIN_DELTA = 100; |
|
|
|
|
private static final long TARGET_CHAIN_HEIGHT = OUR_CHAIN_HEAD_NUMBER + TARGET_CHAIN_DELTA; |
|
|
|
|
private static final UInt256 TARGET_DIFFICULTY = OUR_CHAIN_DIFFICULTY.plus(TARGET_CHAIN_DELTA); |
|
|
|
|
|
|
|
|
|
private final Blockchain blockchain = mock(Blockchain.class); |
|
|
|
|
private final EthPeers ethPeers = mock(EthPeers.class); |
|
|
|
|
private final SyncState.InSyncListener inSyncListener = mock(SyncState.InSyncListener.class); |
|
|
|
|
private final EthPeer peer = mock(EthPeer.class); |
|
|
|
|
private final ChainState peerChainHead = new ChainState(); |
|
|
|
|
private final EthPeer syncTargetPeer = mock(EthPeer.class); |
|
|
|
|
private final ChainState syncTargetPeerChainState = spy(new ChainState()); |
|
|
|
|
private final EthPeer otherPeer = mock(EthPeer.class); |
|
|
|
|
private final ChainState otherPeerChainState = spy(new ChainState()); |
|
|
|
|
private SyncState syncState; |
|
|
|
|
private BlockAddedObserver blockAddedObserver; |
|
|
|
|
|
|
|
|
@ -53,27 +66,137 @@ public class SyncStateTest { |
|
|
|
|
public void setUp() { |
|
|
|
|
final ArgumentCaptor<BlockAddedObserver> captor = |
|
|
|
|
ArgumentCaptor.forClass(BlockAddedObserver.class); |
|
|
|
|
|
|
|
|
|
final ChainHead ourChainHead = |
|
|
|
|
new ChainHead(Hash.ZERO, OUR_CHAIN_DIFFICULTY, OUR_CHAIN_HEAD_NUMBER); |
|
|
|
|
|
|
|
|
|
when(blockchain.observeBlockAdded(captor.capture())).thenReturn(1L); |
|
|
|
|
when(peer.chainState()).thenReturn(peerChainHead); |
|
|
|
|
when(blockchain.getChainHeadBlockNumber()).thenReturn(OUR_CHAIN_HEAD_NUMBER); |
|
|
|
|
when(blockchain.getChainHead()).thenReturn(ourChainHead); |
|
|
|
|
when(syncTargetPeer.chainState()).thenReturn(syncTargetPeerChainState); |
|
|
|
|
when(otherPeer.chainState()).thenReturn(otherPeerChainState); |
|
|
|
|
syncState = new SyncState(blockchain, ethPeers); |
|
|
|
|
blockAddedObserver = captor.getValue(); |
|
|
|
|
syncState.addInSyncListener(inSyncListener); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void shouldBeInSyncWhenNoSyncTargetHasBeenSet() { |
|
|
|
|
public void isInSync_noPeers() { |
|
|
|
|
assertThat(syncState.isInSync()).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_singlePeerWithWorseChainBetterHeight() { |
|
|
|
|
updateChainState(otherPeerChainState, TARGET_CHAIN_HEIGHT, OUR_CHAIN_DIFFICULTY.minus(1L)); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(false).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
|
|
|
|
|
assertThat(syncState.isInSync()).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void shouldSwitchToNotInSyncWhenSyncTargetWithBetterChainSet() { |
|
|
|
|
final BlockHeader bestBlockHeader = targetBlockHeader(); |
|
|
|
|
peerChainHead.update(bestBlockHeader); |
|
|
|
|
syncState.setSyncTarget(peer, bestBlockHeader); |
|
|
|
|
public void isInSync_singlePeerWithWorseChainWorseHeight() { |
|
|
|
|
updateChainState( |
|
|
|
|
otherPeerChainState, OUR_CHAIN_HEAD_NUMBER - 1L, OUR_CHAIN_DIFFICULTY.minus(1L)); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(false).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
|
|
|
|
|
assertThat(syncState.isInSync()).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_singlePeerWithBetterChainWorseHeight() { |
|
|
|
|
updateChainState(otherPeerChainState, OUR_CHAIN_HEAD_NUMBER - 1L, TARGET_DIFFICULTY); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(true).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
|
|
|
|
|
assertThat(syncState.isInSync()).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_singlePeerWithBetterChainBetterHeight() { |
|
|
|
|
updateChainState(otherPeerChainState, TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(true).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
|
|
|
|
|
assertThat(syncState.isInSync()).isFalse(); |
|
|
|
|
verify(inSyncListener).onSyncStatusChanged(false); |
|
|
|
|
verifyNoMoreInteractions(inSyncListener); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_syncTargetWithBetterHeight() { |
|
|
|
|
updateChainState(syncTargetPeerChainState, TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY); |
|
|
|
|
syncState.setSyncTarget(syncTargetPeer, blockHeaderAt(0L)); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.syncTarget()).isPresent(); // Sanity check
|
|
|
|
|
assertThat(syncState.isInSync()).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_syncTargetWithWorseHeight() { |
|
|
|
|
updateChainState(syncTargetPeerChainState, OUR_CHAIN_HEAD_NUMBER - 1L, TARGET_DIFFICULTY); |
|
|
|
|
syncState.setSyncTarget(syncTargetPeer, blockHeaderAt(0L)); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.syncTarget()).isPresent(); // Sanity check
|
|
|
|
|
assertThat(syncState.isInSync()).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_outOfSyncWithTargetAndOutOfSyncWithBestPeer() { |
|
|
|
|
updateChainState(syncTargetPeerChainState, TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY); |
|
|
|
|
syncState.setSyncTarget(syncTargetPeer, blockHeaderAt(0L)); |
|
|
|
|
updateChainState(otherPeerChainState, TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(true).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.isInSync()).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_inSyncWithTargetOutOfSyncWithBestPeer() { |
|
|
|
|
updateChainState( |
|
|
|
|
syncTargetPeerChainState, OUR_CHAIN_HEAD_NUMBER - 1L, OUR_CHAIN_DIFFICULTY.minus(1L)); |
|
|
|
|
syncState.setSyncTarget(syncTargetPeer, blockHeaderAt(0L)); |
|
|
|
|
updateChainState(otherPeerChainState, TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(true).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.isInSync()).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
public void isInSync_inSyncWithTargetInSyncWithBestPeer() { |
|
|
|
|
updateChainState( |
|
|
|
|
syncTargetPeerChainState, OUR_CHAIN_HEAD_NUMBER - 1L, OUR_CHAIN_DIFFICULTY.minus(1L)); |
|
|
|
|
syncState.setSyncTarget(syncTargetPeer, blockHeaderAt(0L)); |
|
|
|
|
updateChainState( |
|
|
|
|
otherPeerChainState, OUR_CHAIN_HEAD_NUMBER - 1L, OUR_CHAIN_DIFFICULTY.minus(1L)); |
|
|
|
|
when(ethPeers.bestPeerWithHeightEstimate()).thenReturn(Optional.of(otherPeer)); |
|
|
|
|
doReturn(false).when(otherPeerChainState).chainIsBetterThan(any()); |
|
|
|
|
|
|
|
|
|
assertThat(syncState.isInSync()).isTrue(); |
|
|
|
|
assertThat(syncState.isInSync(0)).isTrue(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
@ -90,7 +213,7 @@ public class SyncStateTest { |
|
|
|
|
public void shouldBecomeInSyncWhenOurBlockchainCatchesUp() { |
|
|
|
|
setupOutOfSyncState(); |
|
|
|
|
|
|
|
|
|
when(blockchain.getChainHeadBlockNumber()).thenReturn(TARGET_SYNC_NUMBER); |
|
|
|
|
when(blockchain.getChainHeadBlockNumber()).thenReturn(TARGET_CHAIN_HEIGHT); |
|
|
|
|
blockAddedObserver.onBlockAdded( |
|
|
|
|
BlockAddedEvent.createForHeadAdvancement( |
|
|
|
|
new Block( |
|
|
|
@ -118,14 +241,42 @@ public class SyncStateTest { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void setupOutOfSyncState() { |
|
|
|
|
final BlockHeader bestBlockHeader = targetBlockHeader(); |
|
|
|
|
peerChainHead.update(bestBlockHeader); |
|
|
|
|
syncState.setSyncTarget(peer, bestBlockHeader); |
|
|
|
|
updateChainState(syncTargetPeerChainState, TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY); |
|
|
|
|
syncState.setSyncTarget(syncTargetPeer, blockHeaderAt(0L)); |
|
|
|
|
assertThat(syncState.isInSync()).isFalse(); |
|
|
|
|
verify(inSyncListener).onSyncStatusChanged(false); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Updates the chain state, such that the peer will end up with an estimated height of {@code |
|
|
|
|
* blockHeight} and an estimated total difficulty of {@code totalDifficulty} |
|
|
|
|
* |
|
|
|
|
* @param chainState The chain state to update |
|
|
|
|
* @param blockHeight The target estimated block height |
|
|
|
|
* @param totalDifficulty The total difficulty |
|
|
|
|
*/ |
|
|
|
|
private void updateChainState( |
|
|
|
|
final ChainState chainState, final long blockHeight, final UInt256 totalDifficulty) { |
|
|
|
|
// Chain state is updated based on the parent of the announced block
|
|
|
|
|
// So, increment block number by 1 and set block difficulty to zero
|
|
|
|
|
// in order to update to the values we want
|
|
|
|
|
final BlockHeader header = |
|
|
|
|
new BlockHeaderTestFixture() |
|
|
|
|
.number(blockHeight + 1L) |
|
|
|
|
.difficulty(UInt256.ZERO) |
|
|
|
|
.buildHeader(); |
|
|
|
|
chainState.updateForAnnouncedBlock(header, totalDifficulty); |
|
|
|
|
|
|
|
|
|
// Sanity check this logic still holds
|
|
|
|
|
assertThat(chainState.getEstimatedHeight()).isEqualTo(blockHeight); |
|
|
|
|
assertThat(chainState.getEstimatedTotalDifficulty()).isEqualTo(totalDifficulty); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private BlockHeader targetBlockHeader() { |
|
|
|
|
return new BlockHeaderTestFixture().number(TARGET_SYNC_NUMBER).buildHeader(); |
|
|
|
|
return blockHeaderAt(TARGET_CHAIN_HEIGHT); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private BlockHeader blockHeaderAt(final long blockNumber) { |
|
|
|
|
return new BlockHeaderTestFixture().number(blockNumber).buildHeader(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|