diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledBFTNetwork.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledNetwork.java similarity index 81% rename from radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledBFTNetwork.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledNetwork.java index 14ed6c444..acd1ce69f 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledBFTNetwork.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledNetwork.java @@ -19,16 +19,19 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.radixdlt.EpochChangeSender; import com.radixdlt.consensus.BFTEventSender; import com.radixdlt.consensus.CommittedStateSync; -import com.radixdlt.consensus.GetVerticesResponse; +import com.radixdlt.consensus.EpochChange; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; import com.radixdlt.consensus.NewView; import com.radixdlt.consensus.Proposal; import com.radixdlt.consensus.QuorumCertificate; import com.radixdlt.consensus.SyncVerticesRPCSender; import com.radixdlt.consensus.Vertex; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; -import com.radixdlt.consensus.VertexStore.VertexStoreEventSender; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.VertexStoreEventSender; import com.radixdlt.consensus.Vote; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; @@ -43,11 +46,11 @@ * * This class is not thread safe. */ -public final class ControlledBFTNetwork { +public final class ControlledNetwork { private final ImmutableList nodes; private final ImmutableMap> messageQueue; - ControlledBFTNetwork(ImmutableList nodes) { + ControlledNetwork(ImmutableList nodes) { this.nodes = nodes; this.messageQueue = nodes.stream() .flatMap(n0 -> nodes.stream().map(n1 -> new ChannelId(n0, n1))) @@ -158,13 +161,18 @@ public Hash getVertexId() { public int getCount() { return count; } + + @Override + public String toString() { + return String.format("%s{count=%s}", this.getClass().getSimpleName(), count); + } } public ControlledSender getSender(ECPublicKey sender) { return new ControlledSender(sender); } - public final class ControlledSender implements BFTEventSender, VertexStoreEventSender, SyncVerticesRPCSender { + public final class ControlledSender implements BFTEventSender, VertexStoreEventSender, SyncVerticesRPCSender, EpochChangeSender { private final ECPublicKey sender; private ControlledSender(ECPublicKey sender) { @@ -184,7 +192,15 @@ public void sendGetVerticesResponse(GetVerticesRequest originalRequest, Immutabl } @Override - public void syncedVertex(Vertex vertex) { + public void sendGetVerticesErrorResponse(GetVerticesRequest originalRequest, QuorumCertificate highestQC, + QuorumCertificate highestCommittedQC) { + ControlledGetVerticesRequest request = (ControlledGetVerticesRequest) originalRequest; + GetVerticesErrorResponse response = new GetVerticesErrorResponse(request.getVertexId(), highestQC, highestCommittedQC, request.opaque); + putMesssage(new ControlledMessage(sender, request.requestor, response)); + } + + @Override + public void sendSyncedVertex(Vertex vertex) { putMesssage(new ControlledMessage(sender, sender, vertex.getId())); } @@ -210,7 +226,12 @@ public void committedStateSync(CommittedStateSync committedStateSync) { } @Override - public void committedVertex(Vertex vertex) { + public void epochChange(EpochChange epochChange) { + putMesssage(new ControlledMessage(sender, sender, epochChange)); + } + + @Override + public void sendCommittedVertex(Vertex vertex) { // Ignore committed vertex signal } diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledBFTNode.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledNode.java similarity index 50% rename from radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledBFTNode.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledNode.java index 1eccd145f..cc5f5b92e 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledBFTNode.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/ControlledNode.java @@ -19,37 +19,35 @@ import static org.mockito.Mockito.mock; -import com.radixdlt.consensus.BFTEventPreprocessor; -import com.radixdlt.consensus.BFTEventProcessor; +import com.radixdlt.consensus.BFTEventReducer; +import com.radixdlt.consensus.BFTFactory; import com.radixdlt.consensus.CommittedStateSync; +import com.radixdlt.consensus.ConsensusEvent; import com.radixdlt.consensus.DefaultHasher; +import com.radixdlt.consensus.EmptySyncEpochsRPCSender; import com.radixdlt.consensus.EmptySyncVerticesRPCSender; -import com.radixdlt.consensus.GetVerticesResponse; +import com.radixdlt.consensus.EpochChange; +import com.radixdlt.consensus.EpochManager; +import com.radixdlt.consensus.PendingVotes; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; +import com.radixdlt.consensus.LocalTimeout; +import com.radixdlt.consensus.ProposerElectionFactory; import com.radixdlt.consensus.VertexMetadata; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.consensus.Hasher; -import com.radixdlt.consensus.NewView; -import com.radixdlt.consensus.PendingVotes; -import com.radixdlt.consensus.Proposal; -import com.radixdlt.consensus.QuorumCertificate; -import com.radixdlt.consensus.BFTEventReducer; -import com.radixdlt.consensus.SyncQueues; import com.radixdlt.consensus.SyncedStateComputer; import com.radixdlt.consensus.Vertex; -import com.radixdlt.consensus.VertexStore; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.SyncVerticesRPCSender; -import com.radixdlt.consensus.View; -import com.radixdlt.consensus.Vote; -import com.radixdlt.consensus.deterministic.ControlledBFTNetwork.ControlledSender; +import com.radixdlt.consensus.VertexStoreFactory; +import com.radixdlt.consensus.deterministic.ControlledNetwork.ControlledSender; import com.radixdlt.consensus.liveness.FixedTimeoutPacemaker; -import com.radixdlt.consensus.liveness.FixedTimeoutPacemaker.TimeoutSender; import com.radixdlt.consensus.liveness.MempoolProposalGenerator; -import com.radixdlt.consensus.liveness.Pacemaker; import com.radixdlt.consensus.liveness.ProposalGenerator; -import com.radixdlt.consensus.liveness.ProposerElection; +import com.radixdlt.consensus.liveness.ScheduledTimeoutSender; import com.radixdlt.consensus.safety.SafetyRules; import com.radixdlt.consensus.safety.SafetyState; -import com.radixdlt.consensus.validators.Validator; import com.radixdlt.consensus.validators.ValidatorSet; import com.radixdlt.counters.SystemCounters; import com.radixdlt.crypto.ECKeyPair; @@ -59,29 +57,30 @@ import com.radixdlt.mempool.Mempool; import com.radixdlt.middleware2.CommittedAtom; import java.util.List; +import java.util.Objects; import java.util.function.BooleanSupplier; -import java.util.stream.Collectors; /** - * Controlled BFT Node where its state machine is managed by a synchronous + * Controlled Node where its state machine is managed by a synchronous * processNext() call. */ -class ControlledBFTNode { - private final BFTEventProcessor ec; +class ControlledNode { + private final EpochManager epochManager; private final SystemCounters systemCounters; - private final VertexStore vertexStore; + private final ValidatorSet initialValidatorSet; + private final ControlledSender controlledSender; - ControlledBFTNode( + ControlledNode( ECKeyPair key, ControlledSender sender, - ProposerElection proposerElection, - ValidatorSet validatorSet, + ProposerElectionFactory proposerElectionFactory, + ValidatorSet initialValidatorSet, boolean enableGetVerticesRPC, BooleanSupplier syncedSupplier ) { this.systemCounters = SystemCounters.getInstance(); - Vertex genesisVertex = Vertex.createGenesis(); - QuorumCertificate genesisQC = QuorumCertificate.ofGenesis(genesisVertex); + this.controlledSender = Objects.requireNonNull(sender); + this.initialValidatorSet = Objects.requireNonNull(initialValidatorSet); SyncedStateComputer stateComputer = new SyncedStateComputer() { @Override @@ -105,40 +104,41 @@ public void execute(CommittedAtom instruction) { }; SyncVerticesRPCSender syncVerticesRPCSender = enableGetVerticesRPC ? sender : EmptySyncVerticesRPCSender.INSTANCE; - this.vertexStore = new VertexStore(genesisVertex, genesisQC, stateComputer, syncVerticesRPCSender, sender, systemCounters); Mempool mempool = new EmptyMempool(); - ProposalGenerator proposalGenerator = new MempoolProposalGenerator(vertexStore, mempool); - TimeoutSender timeoutSender = mock(TimeoutSender.class); - // Timeout doesn't matter here - Pacemaker pacemaker = new FixedTimeoutPacemaker(1, timeoutSender); Hasher hasher = new DefaultHasher(); - SafetyRules safetyRules = new SafetyRules(key, SafetyState.initialState(), hasher); - PendingVotes pendingVotes = new PendingVotes(hasher); - BFTEventReducer reducer = new BFTEventReducer( - proposalGenerator, - mempool, - sender, - safetyRules, - pacemaker, - vertexStore, - pendingVotes, - proposerElection, - key, - validatorSet, - systemCounters - ); - SyncQueues syncQueues = new SyncQueues( - validatorSet.getValidators().stream().map(Validator::nodeKey).collect(Collectors.toSet()), - systemCounters - ); + VertexStoreFactory vertexStoreFactory = (vertex, qc, syncedStateComputer) -> + new VertexStore(vertex, qc, syncedStateComputer, syncVerticesRPCSender, sender, systemCounters); + BFTFactory bftFactory = + (pacemaker, vertexStore, proposerElection, validatorSet) -> { + final ProposalGenerator proposalGenerator = new MempoolProposalGenerator(vertexStore, mempool); + final SafetyRules safetyRules = new SafetyRules(key, SafetyState.initialState(), hasher); + final PendingVotes pendingVotes = new PendingVotes(hasher); - this.ec = new BFTEventPreprocessor( + return new BFTEventReducer( + proposalGenerator, + mempool, + controlledSender, + safetyRules, + pacemaker, + vertexStore, + pendingVotes, + proposerElection, + key, + validatorSet, + systemCounters + ); + }; + + this.epochManager = new EpochManager( + stateComputer, + EmptySyncEpochsRPCSender.INSTANCE, + mock(ScheduledTimeoutSender.class), + timeoutSender -> new FixedTimeoutPacemaker(1, timeoutSender), + vertexStoreFactory, + proposerElectionFactory, + bftFactory, key.getPublicKey(), - reducer, - pacemaker, - vertexStore, - proposerElection, - syncQueues + systemCounters ); } @@ -147,26 +147,27 @@ SystemCounters getSystemCounters() { } void start() { - ec.start(); + EpochChange epochChange = new EpochChange(VertexMetadata.ofGenesisAncestor(), this.initialValidatorSet); + controlledSender.epochChange(epochChange); } void processNext(Object msg) { - if (msg instanceof GetVerticesRequest) { - vertexStore.processGetVerticesRequest((GetVerticesRequest) msg); + if (msg instanceof EpochChange) { + this.epochManager.processEpochChange((EpochChange) msg); + } else if (msg instanceof GetVerticesRequest) { + this.epochManager.processGetVerticesRequest((GetVerticesRequest) msg); } else if (msg instanceof GetVerticesResponse) { - vertexStore.processGetVerticesResponse((GetVerticesResponse) msg); + this.epochManager.processGetVerticesResponse((GetVerticesResponse) msg); + } else if (msg instanceof GetVerticesErrorResponse) { + this.epochManager.processGetVerticesErrorResponse((GetVerticesErrorResponse) msg); } else if (msg instanceof CommittedStateSync) { - vertexStore.processCommittedStateSync((CommittedStateSync) msg); - } else if (msg instanceof View) { - ec.processLocalTimeout((View) msg); - } else if (msg instanceof NewView) { - ec.processNewView((NewView) msg); - } else if (msg instanceof Proposal) { - ec.processProposal((Proposal) msg); - } else if (msg instanceof Vote) { - ec.processVote((Vote) msg); + this.epochManager.processCommittedStateSync((CommittedStateSync) msg); + } else if (msg instanceof LocalTimeout) { + this.epochManager.processLocalTimeout((LocalTimeout) msg); + } else if (msg instanceof ConsensusEvent) { + this.epochManager.processConsensusEvent((ConsensusEvent) msg); } else if (msg instanceof Hash) { - ec.processLocalSync((Hash) msg); + this.epochManager.processLocalSync((Hash) msg); } else { throw new IllegalStateException("Unknown msg: " + msg); } diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/BFTDeterministicTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/DeterministicTest.java similarity index 62% rename from radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/BFTDeterministicTest.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/DeterministicTest.java index 5807edd0d..c6fb30b58 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/BFTDeterministicTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/DeterministicTest.java @@ -20,8 +20,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import com.google.common.collect.ImmutableList; -import com.radixdlt.consensus.deterministic.ControlledBFTNetwork.ChannelId; -import com.radixdlt.consensus.deterministic.ControlledBFTNetwork.ControlledMessage; +import com.radixdlt.consensus.deterministic.ControlledNetwork.ChannelId; +import com.radixdlt.consensus.deterministic.ControlledNetwork.ControlledMessage; import com.radixdlt.consensus.liveness.WeightedRotatingLeaders; import com.radixdlt.consensus.validators.Validator; import com.radixdlt.consensus.validators.ValidatorSet; @@ -39,21 +39,15 @@ import java.util.stream.Stream; /** - * A deterministic BFT test where each event that occurs in the BFT network + * A deterministic test where each event that occurs in the network * is emitted and processed synchronously by the caller. */ -public class BFTDeterministicTest { - private final ImmutableList nodes; +public final class DeterministicTest { + private final ImmutableList nodes; private final ImmutableList pks; - private final ControlledBFTNetwork network; + private final ControlledNetwork network; - public BFTDeterministicTest(int numNodes, boolean enableGetVerticesRPC) { - this(numNodes, enableGetVerticesRPC, () -> { - throw new UnsupportedOperationException(); - }); - } - - public BFTDeterministicTest(int numNodes, boolean enableGetVerticesRPC, BooleanSupplier syncedSupplier) { + private DeterministicTest(int numNodes, boolean enableGetVerticesRPC, BooleanSupplier syncedSupplier) { ImmutableList keys = Stream.generate(ECKeyPair::generateNew) .limit(numNodes) .sorted(Comparator.comparing(k -> k.getPublicKey().euid()).reversed()) @@ -61,25 +55,59 @@ public BFTDeterministicTest(int numNodes, boolean enableGetVerticesRPC, BooleanS this.pks = keys.stream() .map(ECKeyPair::getPublicKey) .collect(ImmutableList.toImmutableList()); - this.network = new ControlledBFTNetwork(pks); - ValidatorSet validatorSet = ValidatorSet.from( + this.network = new ControlledNetwork(pks); + ValidatorSet initialValidatorSet = ValidatorSet.from( pks.stream().map(pk -> Validator.from(pk, UInt256.ONE)).collect(Collectors.toList()) ); this.nodes = keys.stream() - .map(key -> new ControlledBFTNode( + .map(key -> new ControlledNode( key, network.getSender(key.getPublicKey()), - new WeightedRotatingLeaders(validatorSet, Comparator.comparing(v -> v.nodeKey().euid()), 5), - validatorSet, + vset -> new WeightedRotatingLeaders(vset, Comparator.comparing(v -> v.nodeKey().euid()), 5), + initialValidatorSet, enableGetVerticesRPC, syncedSupplier )) .collect(ImmutableList.toImmutableList()); } + /** + * Creates a new randomly synced BFT/SyncedStateComputer test + * @param numNodes number of nodes in the network + * @param random the randomizer + * @return a deterministic test + */ + public static DeterministicTest createRandomlySyncedBFTAndSyncedStateComputerTest(int numNodes, Random random) { + return new DeterministicTest(numNodes, true, random::nextBoolean); + } + + /** + * Creates a new "always synced BFT" Deterministic test solely on the bft layer, + * + * @param numNodes number of nodes in the network + * @return a deterministic test + */ + public static DeterministicTest createAlwaysSyncedBFTTest(int numNodes) { + return new DeterministicTest(numNodes, true, () -> true); + } + + /** + * Creates a new "non syncing BFT" Deterministic test solely on the bft layer, + * "non syncing BFT" implying that the configuration of the network should never + * require a vertex sync nor a state computer sync + * + * @param numNodes number of nodes in the network + * @return a deterministic test + */ + public static DeterministicTest createNonSyncingBFTTest(int numNodes) { + return new DeterministicTest(numNodes, false, () -> { + throw new IllegalStateException("This is a nonsyncing bft test and should not have required a state sync"); + }); + } + public void start() { - nodes.forEach(ControlledBFTNode::start); + nodes.forEach(ControlledNode::start); } public void processNextMsg(int toIndex, int fromIndex, Class expectedClass) { diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/FProposalDropperResponsiveTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/FProposalDropperResponsiveTest.java index abe730747..1e77a5340 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/FProposalDropperResponsiveTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/FProposalDropperResponsiveTest.java @@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableSet; import com.radixdlt.consensus.Proposal; import com.radixdlt.consensus.View; -import com.radixdlt.consensus.deterministic.BFTDeterministicTest; +import com.radixdlt.consensus.deterministic.DeterministicTest; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,6 +32,7 @@ import org.junit.Test; public class FProposalDropperResponsiveTest { + private static final int NUM_STEPS = 30000; private final Random random = new Random(123456789); @@ -39,9 +40,9 @@ private void runFProposalDropperResponsiveTest(int numNodes, Function> proposalsToDrop = new HashMap<>(); final Map proposalCount = new HashMap<>(); - final BFTDeterministicTest test = new BFTDeterministicTest(numNodes, true, random::nextBoolean); + final DeterministicTest test = DeterministicTest.createAlwaysSyncedBFTTest(numNodes); test.start(); - for (int step = 0; step < 100000; step++) { + for (int step = 0; step < NUM_STEPS; step++) { test.processNextMsg(random, (receiverId, msg) -> { if (msg instanceof Proposal) { final Proposal proposal = (Proposal) msg; diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneProposalDropperResponsiveTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneProposalDropperResponsiveTest.java index c3e324ba7..24b906b54 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneProposalDropperResponsiveTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneProposalDropperResponsiveTest.java @@ -19,7 +19,7 @@ import com.radixdlt.consensus.Proposal; import com.radixdlt.consensus.View; -import com.radixdlt.consensus.deterministic.BFTDeterministicTest; +import com.radixdlt.consensus.deterministic.DeterministicTest; import java.util.HashMap; import java.util.Map; import java.util.Random; @@ -27,6 +27,7 @@ import org.junit.Test; public class OneProposalDropperResponsiveTest { + private static final int NUM_STEPS = 30000; private final Random random = new Random(123456); @@ -34,9 +35,9 @@ private void runOneProposalDropperResponsiveTest(int numNodes, Function proposalToDrop = new HashMap<>(); final Map proposalCount = new HashMap<>(); - final BFTDeterministicTest test = new BFTDeterministicTest(numNodes, true, random::nextBoolean); + final DeterministicTest test = DeterministicTest.createAlwaysSyncedBFTTest(numNodes); test.start(); - for (int step = 0; step < 100000; step++) { + for (int step = 0; step < NUM_STEPS; step++) { test.processNextMsg(random, (receiverId, msg) -> { if (msg instanceof Proposal) { final Proposal proposal = (Proposal) msg; diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneSlowNodeTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneSlowNodeTest.java index 5c860d0c1..17d766235 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneSlowNodeTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/OneSlowNodeTest.java @@ -19,10 +19,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.radixdlt.consensus.EpochChange; import com.radixdlt.consensus.NewView; import com.radixdlt.consensus.Proposal; import com.radixdlt.consensus.Vote; -import com.radixdlt.consensus.deterministic.BFTDeterministicTest; +import com.radixdlt.consensus.deterministic.DeterministicTest; import com.radixdlt.counters.SystemCounters.CounterType; import org.junit.Test; @@ -30,10 +31,14 @@ public class OneSlowNodeTest { @Test public void when_three_fast_nodes_and_one_slow_node_two_cycles__then_missing_parent_should_not_cause_sync_exception() { - final BFTDeterministicTest test = new BFTDeterministicTest(4, false); + final DeterministicTest test = DeterministicTest.createNonSyncingBFTTest(4); test.start(); + test.processNextMsg(1, 1, EpochChange.class); + test.processNextMsg(2, 2, EpochChange.class); + test.processNextMsg(3, 3, EpochChange.class); + for (int curLeader = 1; curLeader <= 2; curLeader++) { test.processNextMsg(curLeader, 1, NewView.class); test.processNextMsg(curLeader, 2, NewView.class); @@ -58,10 +63,14 @@ public void when_three_fast_nodes_and_one_slow_node_two_cycles__then_missing_par */ @Test public void when_three_fast_nodes_and_one_slow_node__then_missing_parent_should_not_cause_exception() { - final BFTDeterministicTest test = new BFTDeterministicTest(4, false); + final DeterministicTest test = DeterministicTest.createNonSyncingBFTTest(4); test.start(); + test.processNextMsg(1, 1, EpochChange.class); + test.processNextMsg(2, 2, EpochChange.class); + test.processNextMsg(3, 3, EpochChange.class); + for (int curLeader = 1; curLeader <= 3; curLeader++) { test.processNextMsg(curLeader, 1, NewView.class); test.processNextMsg(curLeader, 2, NewView.class); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/RandomChannelOrderResponsiveTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/RandomChannelOrderResponsiveTest.java index 3bb1209ed..2138c929e 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/RandomChannelOrderResponsiveTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/bft/synchronous/RandomChannelOrderResponsiveTest.java @@ -17,19 +17,20 @@ package com.radixdlt.consensus.deterministic.tests.bft.synchronous; -import com.radixdlt.consensus.deterministic.BFTDeterministicTest; +import com.radixdlt.consensus.deterministic.DeterministicTest; import java.util.Random; import org.junit.Test; public class RandomChannelOrderResponsiveTest { + private static final int NUM_STEPS = 30000; @Test public void when_run_4_correct_nodes_with_channel_order_random_and_timeouts_disabled__then_bft_should_be_responsive() { final Random random = new Random(12345); - final BFTDeterministicTest test = new BFTDeterministicTest(4, false); + final DeterministicTest test = DeterministicTest.createAlwaysSyncedBFTTest(4); test.start(); - for (int step = 0; step < 100000; step++) { + for (int step = 0; step < NUM_STEPS; step++) { test.processNextMsg(random); } } @@ -37,10 +38,10 @@ public void when_run_4_correct_nodes_with_channel_order_random_and_timeouts_disa @Test public void when_run_100_correct_nodes_with_channel_order_random_and_timeouts_disabled__then_bft_should_be_responsive() { final Random random = new Random(12345); - final BFTDeterministicTest test = new BFTDeterministicTest(100, false); + final DeterministicTest test = DeterministicTest.createAlwaysSyncedBFTTest(100); test.start(); - for (int step = 0; step < 100000; step++) { + for (int step = 0; step < NUM_STEPS; step++) { test.processNextMsg(random); } } diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/syncedstatecomputer/FProposalDropperRandomSyncResponsiveTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/syncedstatecomputer/FProposalDropperRandomSyncResponsiveTest.java new file mode 100644 index 000000000..0d0876e95 --- /dev/null +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/syncedstatecomputer/FProposalDropperRandomSyncResponsiveTest.java @@ -0,0 +1,139 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.deterministic.tests.syncedstatecomputer; + +import com.google.common.collect.ImmutableSet; +import com.radixdlt.consensus.Proposal; +import com.radixdlt.consensus.View; +import com.radixdlt.consensus.deterministic.DeterministicTest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Test; + +public class FProposalDropperRandomSyncResponsiveTest { + private static final int NUM_STEPS = 30000; + + private final Random random = new Random(123456789); + + private void runFProposalDropperResponsiveTest(int numNodes, Function> nodesToDropFunction) { + final Map> proposalsToDrop = new HashMap<>(); + final Map proposalCount = new HashMap<>(); + + final DeterministicTest test = DeterministicTest.createRandomlySyncedBFTAndSyncedStateComputerTest(numNodes, random); + test.start(); + for (int step = 0; step < NUM_STEPS; step++) { + test.processNextMsg(random, (receiverId, msg) -> { + if (msg instanceof Proposal) { + final Proposal proposal = (Proposal) msg; + final View view = proposal.getVertex().getView(); + final Set nodesToDrop = proposalsToDrop.computeIfAbsent(view, nodesToDropFunction); + + if (proposalCount.merge(view, 1, Integer::sum).equals(numNodes)) { + proposalsToDrop.remove(view); + proposalCount.remove(view); + } + + return !nodesToDrop.contains(receiverId); + } + + return true; + }); + } + } + + private void runRandomMaliciousNodesTest(int numNodes) { + this.runFProposalDropperResponsiveTest( + numNodes, + v -> { + List nodes = Stream.iterate(0, a -> a + 1).limit(numNodes).collect(Collectors.toList()); + return Stream.iterate(0, a -> a + 1) + .limit((numNodes - 1) / 3) + .map(i -> { + int index = random.nextInt(nodes.size()); + return nodes.remove(index); + }) + .collect(Collectors.toSet()); + } + ); + } + + + @Test + public void when_run_4_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runRandomMaliciousNodesTest(4); + } + + @Test + public void when_run_5_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runRandomMaliciousNodesTest(5); + } + + @Test + public void when_run_10_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runRandomMaliciousNodesTest(10); + } + + @Test + public void when_run_50_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runRandomMaliciousNodesTest(50); + } + + @Test + public void when_run_100_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runRandomMaliciousNodesTest(100); + } + + private void runStaticMaliciousNodesTest(int numNodes) { + Set nodes = Stream.iterate(0, a -> a + 1).limit((numNodes - 1) / 3).collect(ImmutableSet.toImmutableSet()); + this.runFProposalDropperResponsiveTest( + numNodes, + v -> nodes + ); + } + + @Test + public void when_run_4_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runStaticMaliciousNodesTest(4); + } + + @Test + public void when_run_5_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runStaticMaliciousNodesTest(5); + } + + @Test + public void when_run_10_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runStaticMaliciousNodesTest(10); + } + + @Test + public void when_run_50_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runStaticMaliciousNodesTest(50); + } + + @Test + public void when_run_100_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runStaticMaliciousNodesTest(100); + } +} diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/syncedstatecomputer/OneProposalDropperRandomSyncResponsiveTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/syncedstatecomputer/OneProposalDropperRandomSyncResponsiveTest.java new file mode 100644 index 000000000..8cafd5b3a --- /dev/null +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/deterministic/tests/syncedstatecomputer/OneProposalDropperRandomSyncResponsiveTest.java @@ -0,0 +1,108 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.deterministic.tests.syncedstatecomputer; + +import com.radixdlt.consensus.Proposal; +import com.radixdlt.consensus.View; +import com.radixdlt.consensus.deterministic.DeterministicTest; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import org.junit.Test; + +public class OneProposalDropperRandomSyncResponsiveTest { + private static final int NUM_STEPS = 30000; + + private final Random random = new Random(123456); + + private void runOneProposalDropperResponsiveTest(int numNodes, Function nodeToDropFunction) { + final Map proposalToDrop = new HashMap<>(); + final Map proposalCount = new HashMap<>(); + + final DeterministicTest test = DeterministicTest.createRandomlySyncedBFTAndSyncedStateComputerTest(numNodes, random); + test.start(); + for (int step = 0; step < NUM_STEPS; step++) { + test.processNextMsg(random, (receiverId, msg) -> { + if (msg instanceof Proposal) { + final Proposal proposal = (Proposal) msg; + final View view = proposal.getVertex().getView(); + final Integer nodeToDrop = proposalToDrop.computeIfAbsent(view, nodeToDropFunction); + if (proposalCount.merge(view, 1, Integer::sum).equals(numNodes)) { + proposalToDrop.remove(view); + proposalCount.remove(view); + } + + return !receiverId.equals(nodeToDrop); + } + + return true; + }); + } + } + + @Test + public void when_run_4_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(4, v -> random.nextInt(4)); + } + + @Test + public void when_run_5_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(5, v -> random.nextInt(5)); + } + + @Test + public void when_run_10_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(10, v -> random.nextInt(10)); + } + + @Test + public void when_run_50_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(50, v -> random.nextInt(50)); + } + + @Test + public void when_run_100_correct_nodes_with_random_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(100, v -> random.nextInt(100)); + } + + @Test + public void when_run_4_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(4, v -> 0); + } + + @Test + public void when_run_5_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(5, v -> 0); + } + + @Test + public void when_run_10_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(10, v -> 0); + } + + @Test + public void when_run_50_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(50, v -> 0); + } + + @Test + public void when_run_100_correct_nodes_with_single_node_proposal_dropper_and_timeouts_disabled__then_bft_should_be_responsive() { + this.runOneProposalDropperResponsiveTest(100, v -> 0); + } +} diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/SimulatedTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/SimulationTest.java similarity index 78% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/SimulatedTest.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/SimulationTest.java index 1d3e714dc..3022b8abc 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/SimulatedTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/SimulationTest.java @@ -22,9 +22,10 @@ import com.radixdlt.consensus.View; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; import com.radixdlt.consensus.simulation.invariants.epochs.EpochViewInvariant; -import com.radixdlt.consensus.simulation.network.DroppingLatencyProvider; -import com.radixdlt.consensus.simulation.network.OneProposalPerViewDropper; -import com.radixdlt.consensus.simulation.network.RandomLatencyProvider; +import com.radixdlt.consensus.simulation.configuration.ChangingEpochSyncedStateComputer; +import com.radixdlt.consensus.simulation.configuration.DroppingLatencyProvider; +import com.radixdlt.consensus.simulation.configuration.OneProposalPerViewDropper; +import com.radixdlt.consensus.simulation.configuration.RandomLatencyProvider; import com.radixdlt.consensus.simulation.network.SimulatedNetwork; import com.radixdlt.consensus.simulation.network.SimulatedNetwork.RunningNetwork; import com.radixdlt.consensus.simulation.invariants.bft.AllProposalsHaveDirectParentsInvariant; @@ -32,11 +33,16 @@ import com.radixdlt.consensus.simulation.invariants.bft.NoTimeoutsInvariant; import com.radixdlt.consensus.simulation.invariants.bft.NoneCommittedInvariant; import com.radixdlt.consensus.simulation.invariants.bft.SafetyInvariant; +import com.radixdlt.consensus.simulation.network.SimulatedNetwork.SimulatedStateComputer; +import com.radixdlt.consensus.simulation.configuration.SingleEpochAlwaysSyncedStateComputer; +import com.radixdlt.consensus.validators.Validator; +import com.radixdlt.consensus.validators.ValidatorSet; import com.radixdlt.crypto.ECKeyPair; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.middleware2.network.TestEventCoordinatorNetwork; import com.radixdlt.middleware2.network.TestEventCoordinatorNetwork.LatencyProvider; import com.radixdlt.utils.Pair; +import com.radixdlt.utils.UInt256; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Single; import java.util.Collections; @@ -44,7 +50,10 @@ import java.util.Map; import java.util.Optional; import java.util.Random; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -52,19 +61,21 @@ /** * High level BFT Simulation Test Runner */ -public class SimulatedTest { +public class SimulationTest { private final ImmutableList nodes; private final LatencyProvider latencyProvider; private final ImmutableMap checks; private final int pacemakerTimeout; + private final Function validatorSetMapping; private final boolean getVerticesRPCEnabled; private final View epochHighView; - private SimulatedTest( + private SimulationTest( ImmutableList nodes, LatencyProvider latencyProvider, int pacemakerTimeout, View epochHighView, + Function validatorSetMapping, boolean getVerticesRPCEnabled, ImmutableMap checks ) { @@ -73,6 +84,7 @@ private SimulatedTest( this.checks = checks; this.pacemakerTimeout = pacemakerTimeout; this.epochHighView = epochHighView; + this.validatorSetMapping = validatorSetMapping; this.getVerticesRPCEnabled = getVerticesRPCEnabled; } @@ -83,6 +95,7 @@ public static class Builder { private int pacemakerTimeout = 12 * TestEventCoordinatorNetwork.DEFAULT_LATENCY; private boolean getVerticesRPCEnabled = true; private View epochHighView = null; + private Function> epochToNodeIndexMapper; private Builder() { } @@ -120,6 +133,11 @@ public Builder epochHighView(View epochHighView) { return this; } + public Builder epochToNodesMapper(Function> epochToNodeIndexMapper) { + this.epochToNodeIndexMapper = epochToNodeIndexMapper; + return this; + } + public Builder setGetVerticesRPCEnabled(boolean getVerticesRPCEnabled) { this.getVerticesRPCEnabled = getVerticesRPCEnabled; return this; @@ -165,12 +183,25 @@ public Builder checkEpochHighView(String invariantName, View epochHighView) { return this; } - public SimulatedTest build() { - return new SimulatedTest( + public SimulationTest build() { + final List publicKeys = nodes.stream().map(ECKeyPair::getPublicKey).collect(Collectors.toList()); + Function epochToValidatorSetMapping = + epochToNodeIndexMapper == null + ? epoch -> ValidatorSet.from( + publicKeys.stream() + .map(pk -> Validator.from(pk, UInt256.ONE)) + .collect(Collectors.toList())) + : epochToNodeIndexMapper.andThen(indices -> ValidatorSet.from( + indices.stream() + .map(nodes::get) + .map(kp -> Validator.from(kp.getPublicKey(), UInt256.ONE)) + .collect(Collectors.toList()))); + return new SimulationTest( ImmutableList.copyOf(nodes), latencyProvider.copyOf(), pacemakerTimeout, epochHighView, + epochToValidatorSetMapping, getVerticesRPCEnabled, this.checksBuilder.build() ); @@ -223,7 +254,16 @@ public Map> run(long duration, TimeUnit tim TestEventCoordinatorNetwork network = TestEventCoordinatorNetwork.builder() .latencyProvider(this.latencyProvider) .build(); - SimulatedNetwork bftNetwork = new SimulatedNetwork(nodes, network, pacemakerTimeout, epochHighView, getVerticesRPCEnabled); + + final Supplier stateComputerSupplier; + final List publicKeys = nodes.stream().map(ECKeyPair::getPublicKey).collect(Collectors.toList()); + if (epochHighView == null) { + stateComputerSupplier = () -> new SingleEpochAlwaysSyncedStateComputer(publicKeys); + } else { + stateComputerSupplier = () -> new ChangingEpochSyncedStateComputer(epochHighView, validatorSetMapping); + } + + SimulatedNetwork bftNetwork = new SimulatedNetwork(nodes, network, pacemakerTimeout, stateComputerSupplier, getVerticesRPCEnabled); return bftNetwork.start() .timeout(10, TimeUnit.SECONDS) diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/ChangingEpochSyncedStateComputer.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/ChangingEpochSyncedStateComputer.java similarity index 96% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/ChangingEpochSyncedStateComputer.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/ChangingEpochSyncedStateComputer.java index 5370da146..d07997b35 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/ChangingEpochSyncedStateComputer.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/ChangingEpochSyncedStateComputer.java @@ -15,7 +15,7 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus.simulation.network; +package com.radixdlt.consensus.simulation.configuration; import com.radixdlt.consensus.EpochChange; import com.radixdlt.consensus.Vertex; @@ -37,10 +37,10 @@ * State computer which changes epochs after some number of views */ public class ChangingEpochSyncedStateComputer implements SimulatedStateComputer { - private VertexMetadata lastEpochChange = null; private final Subject epochChanges = BehaviorSubject.create().toSerialized(); - private View epochHighView; + private final View epochHighView; private final Function validatorSetMapping; + private VertexMetadata lastEpochChange = null; public ChangingEpochSyncedStateComputer(View epochHighView, Function validatorSetMapping) { this.epochHighView = Objects.requireNonNull(epochHighView); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/DroppingLatencyProvider.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/DroppingLatencyProvider.java similarity index 97% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/DroppingLatencyProvider.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/DroppingLatencyProvider.java index 03b2e3331..cf55f7045 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/DroppingLatencyProvider.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/DroppingLatencyProvider.java @@ -15,7 +15,7 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus.simulation.network; +package com.radixdlt.consensus.simulation.configuration; import com.google.common.collect.Sets; import com.radixdlt.crypto.ECPublicKey; diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/OneProposalPerViewDropper.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/OneProposalPerViewDropper.java similarity index 97% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/OneProposalPerViewDropper.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/OneProposalPerViewDropper.java index e9786659e..32605938f 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/OneProposalPerViewDropper.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/OneProposalPerViewDropper.java @@ -15,7 +15,7 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus.simulation.network; +package com.radixdlt.consensus.simulation.configuration; import com.google.common.collect.ImmutableList; import com.radixdlt.consensus.Proposal; diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/RandomLatencyProvider.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/RandomLatencyProvider.java similarity index 97% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/RandomLatencyProvider.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/RandomLatencyProvider.java index 0e285e989..2ef0e3b34 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/RandomLatencyProvider.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/RandomLatencyProvider.java @@ -15,7 +15,7 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus.simulation.network; +package com.radixdlt.consensus.simulation.configuration; import com.radixdlt.middleware2.network.TestEventCoordinatorNetwork.LatencyProvider; import com.radixdlt.middleware2.network.TestEventCoordinatorNetwork.MessageInTransit; diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SingleEpochAlwaysSyncedStateComputer.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/SingleEpochAlwaysSyncedStateComputer.java similarity index 97% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SingleEpochAlwaysSyncedStateComputer.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/SingleEpochAlwaysSyncedStateComputer.java index 5e5175885..a130dad97 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SingleEpochAlwaysSyncedStateComputer.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/configuration/SingleEpochAlwaysSyncedStateComputer.java @@ -15,7 +15,7 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus.simulation.network; +package com.radixdlt.consensus.simulation.configuration; import com.radixdlt.consensus.EpochChange; import com.radixdlt.consensus.Vertex; diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/invariants/bft/AllProposalsHaveDirectParentsInvariant.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/invariants/bft/AllProposalsHaveDirectParentsInvariant.java index 5bedf5753..7ae3560cf 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/invariants/bft/AllProposalsHaveDirectParentsInvariant.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/invariants/bft/AllProposalsHaveDirectParentsInvariant.java @@ -45,7 +45,7 @@ public Observable check(RunningNetwork network) { return Observable.merge(correctProposals) .concatMap(v -> { if (!v.hasDirectParent()) { - return Observable.just(new TestInvariantError("Vertex has no direct parent")); + return Observable.just(new TestInvariantError(String.format("Vertex %s has no direct parent", v))); } else { return Observable.empty(); } diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SimulatedNetwork.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SimulatedNetwork.java index 9163b9a98..7f314ef9a 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SimulatedNetwork.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/network/SimulatedNetwork.java @@ -18,37 +18,40 @@ package com.radixdlt.consensus.simulation.network; import com.google.common.collect.ImmutableMap; +import com.radixdlt.consensus.BFTEventReducer; +import com.radixdlt.consensus.BFTFactory; import com.radixdlt.consensus.ConsensusRunner; import com.radixdlt.consensus.ConsensusRunner.Event; import com.radixdlt.consensus.ConsensusRunner.EventType; import com.radixdlt.consensus.DefaultHasher; +import com.radixdlt.consensus.EmptySyncEpochsRPCSender; import com.radixdlt.consensus.EmptySyncVerticesRPCSender; import com.radixdlt.consensus.EpochManager; import com.radixdlt.consensus.EpochChangeRx; import com.radixdlt.consensus.Hasher; import com.radixdlt.consensus.InternalMessagePasser; +import com.radixdlt.consensus.PendingVotes; import com.radixdlt.consensus.SyncedStateComputer; -import com.radixdlt.consensus.VertexStore; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.SyncVerticesRPCSender; import com.radixdlt.consensus.VertexStoreEventsRx; import com.radixdlt.consensus.VertexStoreFactory; -import com.radixdlt.consensus.View; import com.radixdlt.consensus.liveness.FixedTimeoutPacemaker; +import com.radixdlt.consensus.liveness.MempoolProposalGenerator; +import com.radixdlt.consensus.liveness.ProposalGenerator; import com.radixdlt.consensus.liveness.ScheduledTimeoutSender; import com.radixdlt.consensus.liveness.WeightedRotatingLeaders; -import com.radixdlt.consensus.validators.Validator; -import com.radixdlt.consensus.validators.ValidatorSet; +import com.radixdlt.consensus.safety.SafetyRules; +import com.radixdlt.consensus.safety.SafetyState; import com.radixdlt.counters.SystemCounters; import com.radixdlt.counters.SystemCounters.CounterType; import com.radixdlt.crypto.ECKeyPair; -import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.mempool.EmptyMempool; import com.radixdlt.mempool.Mempool; import com.radixdlt.middleware2.CommittedAtom; import com.radixdlt.middleware2.network.TestEventCoordinatorNetwork; import com.radixdlt.middleware2.network.TestEventCoordinatorNetwork.SimulatedNetworkReceiver; -import com.radixdlt.utils.UInt256; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Single; @@ -59,6 +62,7 @@ import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; @@ -76,9 +80,9 @@ public class SimulatedNetwork { private final ImmutableMap runners; private final List nodes; private final boolean getVerticesRPCEnabled; - private final View epochHighView; + private final Supplier stateComputerSupplier; - interface SimulatedStateComputer extends SyncedStateComputer, EpochChangeRx { + public interface SimulatedStateComputer extends SyncedStateComputer, EpochChangeRx { } /** @@ -91,11 +95,11 @@ public SimulatedNetwork( List nodes, TestEventCoordinatorNetwork underlyingNetwork, int pacemakerTimeout, - View epochHighView, + Supplier stateComputerSupplier, boolean getVerticesRPCEnabled ) { this.nodes = nodes; - this.epochHighView = epochHighView; + this.stateComputerSupplier = stateComputerSupplier; this.getVerticesRPCEnabled = getVerticesRPCEnabled; this.underlyingNetwork = Objects.requireNonNull(underlyingNetwork); this.pacemakerTimeout = pacemakerTimeout; @@ -105,24 +109,11 @@ public SimulatedNetwork( } private ConsensusRunner createBFTInstance(ECKeyPair key) { - Mempool mempool = new EmptyMempool(); - Hasher hasher = new DefaultHasher(); - ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(daemonThreads("TimeoutSender")); - ScheduledTimeoutSender timeoutSender = new ScheduledTimeoutSender(scheduledExecutorService); - - List publicKeys = nodes.stream().map(ECKeyPair::getPublicKey).collect(Collectors.toList()); - - final SimulatedStateComputer stateComputer; - if (epochHighView == null) { - stateComputer = new SingleEpochAlwaysSyncedStateComputer(publicKeys); - } else { - stateComputer = new ChangingEpochSyncedStateComputer( - epochHighView, - v -> ValidatorSet.from(publicKeys.stream().map(pk -> Validator.from(pk, UInt256.ONE)).collect(Collectors.toList())) - ); - } - - VertexStoreFactory vertexStoreFactory = (v, qc) -> { + final Mempool mempool = new EmptyMempool(); + final Hasher hasher = new DefaultHasher(); + final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(daemonThreads("TimeoutSender")); + final ScheduledTimeoutSender timeoutSender = new ScheduledTimeoutSender(scheduledExecutorService); + final VertexStoreFactory vertexStoreFactory = (v, qc, stateComputer) -> { SyncVerticesRPCSender syncVerticesRPCSender = getVerticesRPCEnabled ? underlyingNetwork.getVerticesRequestSender(key.getPublicKey()) : EmptySyncVerticesRPCSender.INSTANCE; @@ -136,19 +127,41 @@ private ConsensusRunner createBFTInstance(ECKeyPair key) { ); }; - EpochManager epochManager = new EpochManager( - mempool, - underlyingNetwork.getNetworkSender(key.getPublicKey()), + final SimulatedStateComputer stateComputer = stateComputerSupplier.get(); + BFTFactory bftFactory = + (pacemaker, vertexStore, proposerElection, validatorSet) -> { + final ProposalGenerator proposalGenerator = new MempoolProposalGenerator(vertexStore, mempool); + final SafetyRules safetyRules = new SafetyRules(key, SafetyState.initialState(), hasher); + final PendingVotes pendingVotes = new PendingVotes(hasher); + + return new BFTEventReducer( + proposalGenerator, + mempool, + underlyingNetwork.getNetworkSender(key.getPublicKey()), + safetyRules, + pacemaker, + vertexStore, + pendingVotes, + proposerElection, + key, + validatorSet, + counters.get(key) + ); + }; + + final EpochManager epochManager = new EpochManager( + stateComputer, + EmptySyncEpochsRPCSender.INSTANCE, timeoutSender, timeoutSender1 -> new FixedTimeoutPacemaker(this.pacemakerTimeout, timeoutSender1), vertexStoreFactory, proposers -> new WeightedRotatingLeaders(proposers, Comparator.comparing(v -> v.nodeKey().euid()), 5), - hasher, - key, + bftFactory, + key.getPublicKey(), counters.get(key) ); - SimulatedNetworkReceiver rx = underlyingNetwork.getNetworkRx(key.getPublicKey()); + final SimulatedNetworkReceiver rx = underlyingNetwork.getNetworkRx(key.getPublicKey()); return new ConsensusRunner( stateComputer, diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/FPlusOneOutOfBoundsTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/FPlusOneOutOfBoundsTest.java index 501113663..d6fe730ce 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/FPlusOneOutOfBoundsTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/FPlusOneOutOfBoundsTest.java @@ -20,8 +20,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; -import com.radixdlt.consensus.simulation.SimulatedTest.Builder; +import com.radixdlt.consensus.simulation.SimulationTest; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -31,7 +31,7 @@ public class FPlusOneOutOfBoundsTest { private final int latency = 50; private final int synchronousTimeout = 8 * latency; private final int outOfBoundsLatency = synchronousTimeout; - private final Builder bftTestBuilder = SimulatedTest.builder() + private final Builder bftTestBuilder = SimulationTest.builder() .pacemakerTimeout(2 * synchronousTimeout) .checkSafety("safety") .checkNoneCommitted("noneCommitted"); @@ -41,7 +41,7 @@ public class FPlusOneOutOfBoundsTest { */ @Test public void given_0_out_of_3_nodes_out_of_synchrony_bounds() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .numNodesAndLatencies(3, latency, latency, latency) .build(); @@ -54,7 +54,7 @@ public void given_0_out_of_3_nodes_out_of_synchrony_bounds() { */ @Test public void given_1_out_of_3_nodes_out_of_synchrony_bounds() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .numNodesAndLatencies(3, latency, latency, outOfBoundsLatency) .build(); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneOutOfBoundsTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneOutOfBoundsTest.java index dd6cf5057..4aae8cb9c 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneOutOfBoundsTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneOutOfBoundsTest.java @@ -20,8 +20,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; -import com.radixdlt.consensus.simulation.SimulatedTest.Builder; +import com.radixdlt.consensus.simulation.SimulationTest; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -33,7 +33,7 @@ public class OneOutOfBoundsTest { private final int synchronousTimeout = 8 * latency; private final int outOfBoundsLatency = synchronousTimeout; // TODO: Add 1 timeout check - private final Builder bftTestBuilder = SimulatedTest.builder() + private final Builder bftTestBuilder = SimulationTest.builder() .pacemakerTimeout(synchronousTimeout) .checkLiveness("liveness", 2 * synchronousTimeout, TimeUnit.MILLISECONDS) .checkSafety("safety"); @@ -43,7 +43,7 @@ public class OneOutOfBoundsTest { */ @Test public void given_1_out_of_4_nodes_out_of_synchrony_bounds() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .numNodesAndLatencies(4, latency, latency, latency, outOfBoundsLatency) .build(); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneProposalDropperTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneProposalDropperTest.java index f2ac99bb1..cf9500094 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneProposalDropperTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneProposalDropperTest.java @@ -20,8 +20,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; -import com.radixdlt.consensus.simulation.SimulatedTest.Builder; +import com.radixdlt.consensus.simulation.SimulationTest; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -36,7 +36,7 @@ public class OneProposalDropperTest { private final int maxLatency = 200; private final int trips = 20; private final int synchronousTimeout = maxLatency * trips; - private final Builder bftTestBuilder = SimulatedTest.builder() + private final Builder bftTestBuilder = SimulationTest.builder() .numNodes(4) .randomLatency(minLatency, maxLatency) .pacemakerTimeout(synchronousTimeout) @@ -50,7 +50,7 @@ public class OneProposalDropperTest { */ @Test public void given_get_vertices_disabled__then_test_should_fail_against_drop_proposal_adversary() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .setGetVerticesRPCEnabled(false) .build(); @@ -64,7 +64,7 @@ public void given_get_vertices_disabled__then_test_should_fail_against_drop_prop */ @Test public void given_get_vertices_enabled__then_test_should_succeed_against_drop_proposal_adversary() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .setGetVerticesRPCEnabled(true) .build(); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneSlowNodeTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneSlowNodeTest.java index 4f72d2fd7..37311de84 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneSlowNodeTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/OneSlowNodeTest.java @@ -20,8 +20,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; -import com.radixdlt.consensus.simulation.SimulatedTest.Builder; +import com.radixdlt.consensus.simulation.SimulationTest; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -38,7 +38,7 @@ public class OneSlowNodeTest { private final int maxLatency = 200; private final int trips = 8; private final int synchronousTimeout = maxLatency * trips; - private final Builder bftTestBuilder = SimulatedTest.builder() + private final Builder bftTestBuilder = SimulationTest.builder() .numNodesAndLatencies(4, minLatency, minLatency, minLatency, maxLatency) .pacemakerTimeout(synchronousTimeout) .checkSafety("safety") @@ -51,7 +51,7 @@ public class OneSlowNodeTest { */ @Test public void given_4_nodes_3_fast_and_1_slow_node_and_sync_disabled__then_a_timeout_wont_occur() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .setGetVerticesRPCEnabled(false) .build(); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/RandomLatencyTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/RandomLatencyTest.java index d6647fabe..4789523eb 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/RandomLatencyTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/RandomLatencyTest.java @@ -20,8 +20,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; -import com.radixdlt.consensus.simulation.SimulatedTest.Builder; +import com.radixdlt.consensus.simulation.SimulationTest; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; import java.util.Map; import java.util.Optional; import org.assertj.core.api.AssertionsForClassTypes; @@ -44,7 +44,7 @@ public class RandomLatencyTest { private final int trips = 6; private final int synchronousTimeout = maxLatency * trips; - private Builder bftTestBuilder = SimulatedTest.builder() + private Builder bftTestBuilder = SimulationTest.builder() .randomLatency(minLatency, maxLatency) .setGetVerticesRPCEnabled(false) .pacemakerTimeout(synchronousTimeout) // Since no syncing needed 6*MTT required @@ -58,7 +58,7 @@ public class RandomLatencyTest { */ @Test public void given_3_correct_nodes_in_random_network_and_no_sync__then_all_synchronous_checks_should_pass() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .numNodes(3) .build(); @@ -71,7 +71,7 @@ public void given_3_correct_nodes_in_random_network_and_no_sync__then_all_synchr */ @Test public void given_4_correct_bfts_in_random_network_and_no_sync__then_all_synchronous_checks_should_pass() { - SimulatedTest test = bftTestBuilder + SimulationTest test = bftTestBuilder .numNodes(4) .build(); diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/UniformLatencyTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/UniformLatencyTest.java index 3909806a0..676831021 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/UniformLatencyTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/bft/synchronous/UniformLatencyTest.java @@ -20,7 +20,7 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; +import com.radixdlt.consensus.simulation.SimulationTest; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -41,7 +41,7 @@ public class UniformLatencyTest { */ @Test public void given_4_correct_bfts__then_should_pass_sanity_tests_over_1_minute() { - SimulatedTest bftTest = SimulatedTest.builder() + SimulationTest bftTest = SimulationTest.builder() .numNodes(4) .checkSafety("safety") .checkLiveness("liveness") diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/OneValidatorChangePerEpochTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/OneValidatorChangePerEpochTest.java new file mode 100644 index 000000000..e993db5db --- /dev/null +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/OneValidatorChangePerEpochTest.java @@ -0,0 +1,55 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.simulation.tests.epochs; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import com.google.common.collect.Sets; +import com.radixdlt.consensus.View; +import com.radixdlt.consensus.simulation.SimulationTest; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; +import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.Ignore; +import org.junit.Test; + +public class OneValidatorChangePerEpochTest { + private final Builder bftTestBuilder = SimulationTest.builder() + .numNodes(4) + .checkSafety("safety") + .checkLiveness("liveness") + .checkNoTimeouts("noTimeouts") + .checkAllProposalsHaveDirectParents("directParents"); + + @Test + @Ignore + public void given_correct_bft_with_changing_epochs_per_100_views__then_should_pass_bft_and_epoch_invariants() { + SimulationTest bftTest = bftTestBuilder + .epochHighView(View.of(100)) + .epochToNodesMapper(epoch -> + Sets.newHashSet((int) (epoch % 4), (int) (epoch + 1) % 4, (int) (epoch + 2) % 4) + ) + .checkEpochHighView("epochHighView", View.of(100)) + .build(); + Map> results = bftTest.run(1, TimeUnit.MINUTES); + assertThat(results).allSatisfy((name, err) -> AssertionsForClassTypes.assertThat(err).isEmpty()); + } +} diff --git a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/EpochsStaticValidatorTest.java b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/StaticValidatorsTest.java similarity index 87% rename from radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/EpochsStaticValidatorTest.java rename to radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/StaticValidatorsTest.java index d64078fa8..288aa6161 100644 --- a/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/EpochsStaticValidatorTest.java +++ b/radixdlt/src/integration/java/com/radixdlt/consensus/simulation/tests/epochs/StaticValidatorsTest.java @@ -20,16 +20,16 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import com.radixdlt.consensus.View; -import com.radixdlt.consensus.simulation.SimulatedTest.Builder; +import com.radixdlt.consensus.simulation.SimulationTest.Builder; import com.radixdlt.consensus.simulation.TestInvariant.TestInvariantError; -import com.radixdlt.consensus.simulation.SimulatedTest; +import com.radixdlt.consensus.simulation.SimulationTest; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.Test; -public class EpochsStaticValidatorTest { - private final Builder bftTestBuilder = SimulatedTest.builder() +public class StaticValidatorsTest { + private final Builder bftTestBuilder = SimulationTest.builder() .numNodes(4) .checkSafety("safety") .checkLiveness("liveness", 1000, TimeUnit.MILLISECONDS) @@ -38,7 +38,7 @@ public class EpochsStaticValidatorTest { @Test public void given_correct_bft_with_changing_epochs_every_view__then_should_pass_bft_and_epoch_invariants() { - SimulatedTest bftTest = bftTestBuilder + SimulationTest bftTest = bftTestBuilder .pacemakerTimeout(1000) .epochHighView(View.of(1)) .checkEpochHighView("epochHighView", View.of(1)) @@ -49,7 +49,7 @@ public void given_correct_bft_with_changing_epochs_every_view__then_should_pass_ @Test public void given_correct_bft_with_changing_epochs_per_100_views__then_should_fail_incorrect_epoch_invariant() { - SimulatedTest bftTest = bftTestBuilder + SimulationTest bftTest = bftTestBuilder .epochHighView(View.of(100)) .checkEpochHighView("epochHighView", View.of(99)) .build(); @@ -59,7 +59,7 @@ public void given_correct_bft_with_changing_epochs_per_100_views__then_should_fa @Test public void given_correct_bft_with_changing_epochs_per_100_views__then_should_pass_bft_and_epoch_invariants() { - SimulatedTest bftTest = bftTestBuilder + SimulationTest bftTest = bftTestBuilder .epochHighView(View.of(100)) .checkEpochHighView("epochHighView", View.of(100)) .build(); diff --git a/radixdlt/src/main/java/com/radixdlt/CerberusModule.java b/radixdlt/src/main/java/com/radixdlt/CerberusModule.java index c26a765fe..abce95d14 100644 --- a/radixdlt/src/main/java/com/radixdlt/CerberusModule.java +++ b/radixdlt/src/main/java/com/radixdlt/CerberusModule.java @@ -22,30 +22,39 @@ import com.google.inject.Scopes; import com.google.inject.Singleton; import com.google.inject.name.Named; +import com.radixdlt.consensus.BFTEventReducer; import com.radixdlt.consensus.BFTEventSender; import com.radixdlt.consensus.AddressBookValidatorSetProvider; +import com.radixdlt.consensus.BFTFactory; import com.radixdlt.consensus.CommittedStateSyncRx; import com.radixdlt.consensus.ConsensusRunner; import com.radixdlt.consensus.DefaultHasher; +import com.radixdlt.consensus.EmptySyncEpochsRPCSender; import com.radixdlt.consensus.EpochChangeRx; import com.radixdlt.consensus.EpochManager; import com.radixdlt.consensus.EventCoordinatorNetworkRx; +import com.radixdlt.consensus.PendingVotes; +import com.radixdlt.consensus.SyncEpochsRPCSender; import com.radixdlt.consensus.SyncedStateComputer; import com.radixdlt.consensus.VertexStoreEventsRx; import com.radixdlt.consensus.InternalMessagePasser; import com.radixdlt.consensus.ProposerElectionFactory; import com.radixdlt.consensus.Hasher; import com.radixdlt.consensus.SyncVerticesRPCRx; -import com.radixdlt.consensus.VertexStore; -import com.radixdlt.consensus.VertexStore.VertexStoreEventSender; +import com.radixdlt.consensus.bft.VertexStore; +import com.radixdlt.consensus.bft.VertexStore.VertexStoreEventSender; import com.radixdlt.consensus.SyncVerticesRPCSender; import com.radixdlt.consensus.VertexStoreFactory; import com.radixdlt.consensus.View; import com.radixdlt.consensus.liveness.FixedTimeoutPacemaker; +import com.radixdlt.consensus.liveness.MempoolProposalGenerator; import com.radixdlt.consensus.liveness.PacemakerFactory; import com.radixdlt.consensus.liveness.PacemakerRx; +import com.radixdlt.consensus.liveness.ProposalGenerator; import com.radixdlt.consensus.liveness.ScheduledTimeoutSender; import com.radixdlt.consensus.liveness.WeightedRotatingLeaders; +import com.radixdlt.consensus.safety.SafetyRules; +import com.radixdlt.consensus.safety.SafetyState; import com.radixdlt.consensus.sync.StateSyncNetwork; import com.radixdlt.consensus.sync.SyncedRadixEngine; import com.radixdlt.consensus.sync.SyncedRadixEngine.CommittedStateSyncSender; @@ -104,27 +113,67 @@ protected void configure() { @Provides @Singleton - private EpochManager epochManager( + private SyncEpochsRPCSender syncEpochsRPCSender() { + return EmptySyncEpochsRPCSender.INSTANCE; + } + + @Provides + @Singleton + private BFTFactory bftFactory( + BFTEventSender bftEventSender, Mempool mempool, - BFTEventSender sender, + @Named("self") ECKeyPair selfKey, + Hasher hasher, + SystemCounters counters + ) { + return ( + pacemaker, + vertexStore, + proposerElection, + validatorSet + ) -> { + final ProposalGenerator proposalGenerator = new MempoolProposalGenerator(vertexStore, mempool); + final SafetyRules safetyRules = new SafetyRules(selfKey, SafetyState.initialState(), hasher); + final PendingVotes pendingVotes = new PendingVotes(hasher); + + return new BFTEventReducer( + proposalGenerator, + mempool, + bftEventSender, + safetyRules, + pacemaker, + vertexStore, + pendingVotes, + proposerElection, + selfKey, + validatorSet, + counters + ); + }; + } + + @Provides + @Singleton + private EpochManager epochManager( + SyncedRadixEngine syncedRadixEngine, + BFTFactory bftFactory, + SyncEpochsRPCSender syncEpochsRPCSender, ScheduledTimeoutSender scheduledTimeoutSender, PacemakerFactory pacemakerFactory, VertexStoreFactory vertexStoreFactory, ProposerElectionFactory proposerElectionFactory, - Hasher hasher, @Named("self") ECKeyPair selfKey, SystemCounters counters ) { - return new EpochManager( - mempool, - sender, + syncedRadixEngine, + syncEpochsRPCSender, scheduledTimeoutSender, pacemakerFactory, vertexStoreFactory, proposerElectionFactory, - hasher, - selfKey, + bftFactory, + selfKey.getPublicKey(), counters ); } @@ -212,12 +261,11 @@ private PacemakerFactory pacemakerFactory() { @Provides @Singleton private VertexStoreFactory vertexStoreFactory( - SyncedRadixEngine syncedRadixEngine, SyncVerticesRPCSender syncVerticesRPCSender, VertexStoreEventSender vertexStoreEventSender, SystemCounters counters ) { - return (genesisVertex, genesisQC) -> new VertexStore( + return (genesisVertex, genesisQC, syncedRadixEngine) -> new VertexStore( genesisVertex, genesisQC, syncedRadixEngine, diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventPreprocessor.java b/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventPreprocessor.java index 601ec11fd..8bfb0cd82 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventPreprocessor.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventPreprocessor.java @@ -18,6 +18,7 @@ package com.radixdlt.consensus; import com.radixdlt.consensus.SyncQueues.SyncQueue; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.liveness.PacemakerState; import com.radixdlt.consensus.liveness.ProposerElection; import com.radixdlt.crypto.ECPublicKey; diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventReducer.java b/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventReducer.java index cb3cffa04..bf7282f24 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventReducer.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/BFTEventReducer.java @@ -19,6 +19,7 @@ import com.google.inject.Inject; import com.google.inject.name.Named; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.liveness.ProposalGenerator; import com.radixdlt.identifiers.EUID; import com.radixdlt.consensus.liveness.Pacemaker; diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/BFTFactory.java b/radixdlt/src/main/java/com/radixdlt/consensus/BFTFactory.java new file mode 100644 index 000000000..c536905fe --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/BFTFactory.java @@ -0,0 +1,40 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus; + +import com.radixdlt.consensus.bft.VertexStore; +import com.radixdlt.consensus.liveness.Pacemaker; +import com.radixdlt.consensus.liveness.ProposerElection; +import com.radixdlt.consensus.validators.ValidatorSet; + +/** + * Creates a new bft processor + */ +public interface BFTFactory { + /** + * Create a new clean BFT processor + * + * @return a new bft processor + */ + BFTEventProcessor create( + Pacemaker pacemaker, + VertexStore vertexStore, + ProposerElection proposerElection, + ValidatorSet validatorSet + ); +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusEvent.java b/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusEvent.java index 95d0bd6c0..7796f4e3f 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusEvent.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusEvent.java @@ -17,6 +17,8 @@ package com.radixdlt.consensus; +import com.radixdlt.crypto.ECPublicKey; + /** * A message meant for consensus. Currently a marker interface so that all consensus * related messages can be handled within a single rxjava stream. @@ -29,4 +31,10 @@ public interface ConsensusEvent { * @return the epoch number */ long getEpoch(); + + /** + * Get the node author of this consensus message + * @return the node author + */ + ECPublicKey getAuthor(); } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusRunner.java b/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusRunner.java index 2eb6ee080..8d01e9e6f 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusRunner.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/ConsensusRunner.java @@ -49,6 +49,7 @@ public enum EventType { CONSENSUS_EVENT, GET_VERTICES_REQUEST, GET_VERTICES_RESPONSE, + GET_VERTICES_ERROR_RESPONSE, } public static class Event { @@ -121,6 +122,12 @@ public ConsensusRunner( epochManager.processGetVerticesResponse(e); return new Event(EventType.GET_VERTICES_RESPONSE, e); }), + rpcRx.errorResponses() + .observeOn(singleThreadScheduler) + .map(e -> { + epochManager.processGetVerticesErrorResponse(e); + return new Event(EventType.GET_VERTICES_ERROR_RESPONSE, e); + }), vertexStoreEventsRx.syncedVertices() .observeOn(singleThreadScheduler) .map(e -> { diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/EmptyBFTEventProcessor.java b/radixdlt/src/main/java/com/radixdlt/consensus/EmptyBFTEventProcessor.java index 47b161e21..fcf32f667 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/EmptyBFTEventProcessor.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/EmptyBFTEventProcessor.java @@ -22,7 +22,9 @@ /** * An empty BFT event processor */ -public class EmptyBFTEventProcessor implements BFTEventProcessor { +public enum EmptyBFTEventProcessor implements BFTEventProcessor { + INSTANCE; + @Override public void processVote(Vote vote) { // No-op diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncEpochsRPCSender.java b/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncEpochsRPCSender.java new file mode 100644 index 000000000..0b8a043dc --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncEpochsRPCSender.java @@ -0,0 +1,37 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus; + +import com.radixdlt.crypto.ECPublicKey; + +/** + * A mocked sync epochs rpc sender + */ +public enum EmptySyncEpochsRPCSender implements SyncEpochsRPCSender { + INSTANCE; + + @Override + public void sendGetEpochRequest(ECPublicKey peer, long epoch) { + // No-op + } + + @Override + public void sendGetEpochResponse(ECPublicKey peer, VertexMetadata ancestor) { + // No-op + } +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncVerticesRPCSender.java b/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncVerticesRPCSender.java index 8c1797834..6ee87d753 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncVerticesRPCSender.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/EmptySyncVerticesRPCSender.java @@ -18,7 +18,7 @@ package com.radixdlt.consensus; import com.google.common.collect.ImmutableList; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; @@ -37,4 +37,10 @@ public void sendGetVerticesRequest(Hash id, ECPublicKey node, int count, Object public void sendGetVerticesResponse(GetVerticesRequest originalRequest, ImmutableList vertices) { // empty } + + @Override + public void sendGetVerticesErrorResponse(GetVerticesRequest originalRequest, QuorumCertificate highestQC, + QuorumCertificate highestCommittedQC) { + // empty + } } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/EmptyVertexStoreEventProcessor.java b/radixdlt/src/main/java/com/radixdlt/consensus/EmptyVertexStoreEventProcessor.java new file mode 100644 index 000000000..db0606db0 --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/EmptyVertexStoreEventProcessor.java @@ -0,0 +1,49 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus; + +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; + +/** + * An empty/mocked vertex store event processor + */ +public enum EmptyVertexStoreEventProcessor implements VertexStoreEventProcessor { + INSTANCE; + + @Override + public void processGetVerticesRequest(GetVerticesRequest request) { + // No-op + } + + @Override + public void processGetVerticesErrorResponse(GetVerticesErrorResponse response) { + // No-op + } + + @Override + public void processGetVerticesResponse(GetVerticesResponse response) { + // No-op + } + + @Override + public void processCommittedStateSync(CommittedStateSync committedStateSync) { + // No-op + } +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/EpochManager.java b/radixdlt/src/main/java/com/radixdlt/consensus/EpochManager.java index a32d0820c..75fe24de8 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/EpochManager.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/EpochManager.java @@ -18,23 +18,29 @@ package com.radixdlt.consensus; import com.google.common.collect.ImmutableSet; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; +import com.radixdlt.consensus.epoch.GetEpochRequest; +import com.radixdlt.consensus.epoch.GetEpochResponse; import com.radixdlt.consensus.liveness.FixedTimeoutPacemaker.TimeoutSender; -import com.radixdlt.consensus.liveness.MempoolProposalGenerator; import com.radixdlt.consensus.liveness.Pacemaker; import com.radixdlt.consensus.liveness.PacemakerFactory; -import com.radixdlt.consensus.liveness.ProposalGenerator; import com.radixdlt.consensus.liveness.ProposerElection; import com.radixdlt.consensus.liveness.ScheduledTimeoutSender; -import com.radixdlt.consensus.safety.SafetyRules; -import com.radixdlt.consensus.safety.SafetyState; import com.radixdlt.consensus.validators.Validator; import com.radixdlt.consensus.validators.ValidatorSet; import com.radixdlt.counters.SystemCounters; import com.radixdlt.counters.SystemCounters.CounterType; -import com.radixdlt.crypto.ECKeyPair; +import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; -import com.radixdlt.mempool.Mempool; +import com.radixdlt.middleware2.CommittedAtom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; import javax.annotation.concurrent.NotThreadSafe; import org.apache.logging.log4j.LogManager; @@ -44,157 +50,205 @@ * Manages Epochs and the BFT instance (which is mostly epoch agnostic) associated with each epoch */ @NotThreadSafe -public class EpochManager { +public final class EpochManager { private static final Logger log = LogManager.getLogger("EM"); - private static final BFTEventProcessor EMPTY_PROCESSOR = new EmptyBFTEventProcessor(); - private final Mempool mempool; - private final BFTEventSender sender; + private final SyncEpochsRPCSender epochsRPCSender; private final PacemakerFactory pacemakerFactory; private final VertexStoreFactory vertexStoreFactory; private final ProposerElectionFactory proposerElectionFactory; - private final ECKeyPair selfKey; + private final ECPublicKey selfPublicKey; private final SystemCounters counters; - private final Hasher hasher; private final ScheduledTimeoutSender scheduledTimeoutSender; + private final SyncedStateComputer syncedStateComputer; + private final Map> queuedEvents; + private final BFTFactory bftFactory; - private long currentEpoch; - private VertexStore vertexStore; - private BFTEventProcessor eventProcessor = EMPTY_PROCESSOR; + private VertexMetadata currentAncestor; + private VertexStoreEventProcessor vertexStoreEventProcessor = EmptyVertexStoreEventProcessor.INSTANCE; + private BFTEventProcessor bftEventProcessor = EmptyBFTEventProcessor.INSTANCE; + private int numQueuedConsensusEvents = 0; public EpochManager( - Mempool mempool, - BFTEventSender sender, + SyncedStateComputer syncedStateComputer, + SyncEpochsRPCSender epochsRPCSender, ScheduledTimeoutSender scheduledTimeoutSender, PacemakerFactory pacemakerFactory, VertexStoreFactory vertexStoreFactory, ProposerElectionFactory proposerElectionFactory, - Hasher hasher, - ECKeyPair selfKey, + BFTFactory bftFactory, + ECPublicKey selfPublicKey, SystemCounters counters ) { - this.mempool = Objects.requireNonNull(mempool); - this.sender = Objects.requireNonNull(sender); + this.syncedStateComputer = Objects.requireNonNull(syncedStateComputer); + this.epochsRPCSender = Objects.requireNonNull(epochsRPCSender); this.scheduledTimeoutSender = Objects.requireNonNull(scheduledTimeoutSender); this.pacemakerFactory = Objects.requireNonNull(pacemakerFactory); this.vertexStoreFactory = Objects.requireNonNull(vertexStoreFactory); this.proposerElectionFactory = Objects.requireNonNull(proposerElectionFactory); - this.selfKey = Objects.requireNonNull(selfKey); + this.bftFactory = bftFactory; + this.selfPublicKey = Objects.requireNonNull(selfPublicKey); this.counters = Objects.requireNonNull(counters); - this.hasher = Objects.requireNonNull(hasher); + this.queuedEvents = new HashMap<>(); + } + + private long currentEpoch() { + return (this.currentAncestor == null) ? 0 : this.currentAncestor.getEpoch() + 1; } public void processEpochChange(EpochChange epochChange) { ValidatorSet validatorSet = epochChange.getValidatorSet(); - ProposerElection proposerElection = proposerElectionFactory.create(validatorSet); + log.info("NEXT_EPOCH: {}", epochChange); - log.info("NEXT_EPOCH: {} {}", epochChange, proposerElection); VertexMetadata ancestorMetadata = epochChange.getAncestor(); Vertex genesisVertex = Vertex.createGenesis(ancestorMetadata); final long nextEpoch = genesisVertex.getEpoch(); // Sanity check - if (nextEpoch <= this.currentEpoch) { + if (nextEpoch <= this.currentEpoch()) { throw new IllegalStateException("Epoch change has already occurred: " + epochChange); } - counters.set(CounterType.EPOCHS_EPOCH, nextEpoch); - - TimeoutSender sender = (view, ms) -> scheduledTimeoutSender.scheduleTimeout(new LocalTimeout(nextEpoch, view), ms); - Pacemaker pacemaker = pacemakerFactory.create(sender); - SafetyRules safetyRules = new SafetyRules(this.selfKey, SafetyState.initialState(), this.hasher); - PendingVotes pendingVotes = new PendingVotes(this.hasher); - - QuorumCertificate genesisQC = QuorumCertificate.ofGenesis(genesisVertex); - - this.vertexStore = vertexStoreFactory.create(genesisVertex, genesisQC); - ProposalGenerator proposalGenerator = new MempoolProposalGenerator(this.vertexStore, this.mempool); - - BFTEventReducer reducer = new BFTEventReducer( - proposalGenerator, - this.mempool, - this.sender, - safetyRules, - pacemaker, - vertexStore, - pendingVotes, - proposerElection, - this.selfKey, - validatorSet, - counters - ); - - SyncQueues syncQueues = new SyncQueues( - validatorSet.getValidators().stream() - .map(Validator::nodeKey) - .collect(ImmutableSet.toImmutableSet()), - counters - ); - - this.currentEpoch = nextEpoch; - this.eventProcessor = new BFTEventPreprocessor( - this.selfKey.getPublicKey(), - reducer, - pacemaker, - this.vertexStore, - proposerElection, - syncQueues - ); - this.eventProcessor.start(); - } + this.currentAncestor = ancestorMetadata; + this.counters.set(CounterType.EPOCH_MANAGER_EPOCH, nextEpoch); - public void processGetVerticesRequest(GetVerticesRequest request) { - if (this.vertexStore == null) { - return; + final BFTEventProcessor bftEventProcessor; + final VertexStoreEventProcessor vertexStoreEventProcessor; + + if (!validatorSet.containsKey(selfPublicKey)) { + log.info("NEXT_EPOCH: Not a validator"); + bftEventProcessor = EmptyBFTEventProcessor.INSTANCE; + vertexStoreEventProcessor = EmptyVertexStoreEventProcessor.INSTANCE; + } else { + ProposerElection proposerElection = proposerElectionFactory.create(validatorSet); + TimeoutSender sender = (view, ms) -> scheduledTimeoutSender.scheduleTimeout(new LocalTimeout(nextEpoch, view), ms); + Pacemaker pacemaker = pacemakerFactory.create(sender); + QuorumCertificate genesisQC = QuorumCertificate.ofGenesis(genesisVertex); + VertexStore vertexStore = vertexStoreFactory.create(genesisVertex, genesisQC, syncedStateComputer); + BFTEventProcessor reducer = bftFactory.create( + pacemaker, + vertexStore, + proposerElection, + validatorSet + ); + SyncQueues syncQueues = new SyncQueues( + validatorSet.getValidators().stream() + .map(Validator::nodeKey) + .collect(ImmutableSet.toImmutableSet()), + this.counters + ); + + vertexStoreEventProcessor = vertexStore; + bftEventProcessor = new BFTEventPreprocessor( + this.selfPublicKey, + reducer, + pacemaker, + vertexStore, + proposerElection, + syncQueues + ); } - vertexStore.processGetVerticesRequest(request); - } + // Update processors + this.bftEventProcessor = bftEventProcessor; + this.vertexStoreEventProcessor = vertexStoreEventProcessor; - public void processGetVerticesResponse(GetVerticesResponse response) { - if (this.vertexStore == null) { - return; + this.bftEventProcessor.start(); + + // Execute any queued up consensus events + final List queuedEventsForEpoch = queuedEvents.getOrDefault(nextEpoch, Collections.emptyList()); + for (ConsensusEvent consensusEvent : queuedEventsForEpoch) { + this.processConsensusEventInternal(consensusEvent); } - vertexStore.processGetVerticesResponse(response); + numQueuedConsensusEvents -= queuedEventsForEpoch.size(); + counters.set(CounterType.EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS, numQueuedConsensusEvents); + queuedEvents.remove(nextEpoch); } - public void processConsensusEvent(ConsensusEvent consensusEvent) { - // TODO: Add the rest of consensus event verification here including signature verification + public void processGetEpochRequest(GetEpochRequest request) { + if (this.currentEpoch() == request.getEpoch()) { + epochsRPCSender.sendGetEpochResponse(request.getSender(), this.currentAncestor); + } else { + // TODO: Send better error message back + epochsRPCSender.sendGetEpochResponse(request.getSender(), null); + } + } - if (consensusEvent.getEpoch() != this.currentEpoch) { - log.warn("Received event not in the current epoch ({}): {}", this.currentEpoch, consensusEvent); + public void processGetEpochResponse(GetEpochResponse response) { + if (response.getEpochAncestor() == null) { + log.warn("Received empty GetEpochResponse {}", response); return; } + final VertexMetadata ancestor = response.getEpochAncestor(); + if (ancestor.getEpoch() + 1 > this.currentEpoch()) { + syncedStateComputer.syncTo(ancestor, Collections.singletonList(response.getSender()), null); + } + } + + private void processConsensusEventInternal(ConsensusEvent consensusEvent) { if (consensusEvent instanceof NewView) { - eventProcessor.processNewView((NewView) consensusEvent); + bftEventProcessor.processNewView((NewView) consensusEvent); } else if (consensusEvent instanceof Proposal) { - eventProcessor.processProposal((Proposal) consensusEvent); + bftEventProcessor.processProposal((Proposal) consensusEvent); } else if (consensusEvent instanceof Vote) { - eventProcessor.processVote((Vote) consensusEvent); + bftEventProcessor.processVote((Vote) consensusEvent); } else { throw new IllegalStateException("Unknown consensus event: " + consensusEvent); } } + public void processConsensusEvent(ConsensusEvent consensusEvent) { + // TODO: Add the rest of consensus event verification here including signature verification + + if (consensusEvent.getEpoch() > this.currentEpoch()) { + log.warn("Received higher epoch event {} from current epoch: {}", consensusEvent, this.currentEpoch()); + + // queue higher epoch events for later processing + // TODO: need to clear this by some rule (e.g. timeout or max size) or else memory leak attack possible + queuedEvents.computeIfAbsent(consensusEvent.getEpoch(), e -> new ArrayList<>()).add(consensusEvent); + numQueuedConsensusEvents++; + counters.set(CounterType.EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS, numQueuedConsensusEvents); + + // Send request for higher epoch proof + epochsRPCSender.sendGetEpochRequest(consensusEvent.getAuthor(), this.currentEpoch() + 1); + return; + } + + if (consensusEvent.getEpoch() < this.currentEpoch()) { + log.warn("Received lower epoch event {} from current epoch: {}", consensusEvent, this.currentEpoch()); + return; + } + + this.processConsensusEventInternal(consensusEvent); + } + public void processLocalTimeout(LocalTimeout localTimeout) { - if (localTimeout.getEpoch() != this.currentEpoch) { + if (localTimeout.getEpoch() != this.currentEpoch()) { return; } - eventProcessor.processLocalTimeout(localTimeout.getView()); + bftEventProcessor.processLocalTimeout(localTimeout.getView()); } public void processLocalSync(Hash synced) { - eventProcessor.processLocalSync(synced); + bftEventProcessor.processLocalSync(synced); } - public void processCommittedStateSync(CommittedStateSync committedStateSync) { - if (vertexStore == null) { - return; - } + public void processGetVerticesRequest(GetVerticesRequest request) { + vertexStoreEventProcessor.processGetVerticesRequest(request); + } + + public void processGetVerticesErrorResponse(GetVerticesErrorResponse response) { + vertexStoreEventProcessor.processGetVerticesErrorResponse(response); + } - vertexStore.processCommittedStateSync(committedStateSync); + public void processGetVerticesResponse(GetVerticesResponse response) { + vertexStoreEventProcessor.processGetVerticesResponse(response); + } + + public void processCommittedStateSync(CommittedStateSync committedStateSync) { + vertexStoreEventProcessor.processCommittedStateSync(committedStateSync); } } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/InternalMessagePasser.java b/radixdlt/src/main/java/com/radixdlt/consensus/InternalMessagePasser.java index fb2f31002..8b68ef245 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/InternalMessagePasser.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/InternalMessagePasser.java @@ -18,7 +18,7 @@ package com.radixdlt.consensus; import com.radixdlt.EpochChangeSender; -import com.radixdlt.consensus.VertexStore.VertexStoreEventSender; +import com.radixdlt.consensus.bft.VertexStore.VertexStoreEventSender; import com.radixdlt.consensus.sync.SyncedRadixEngine.CommittedStateSyncSender; import com.radixdlt.crypto.Hash; import io.reactivex.rxjava3.core.Observable; @@ -59,7 +59,7 @@ public Observable highQCs() { } @Override - public void syncedVertex(Vertex vertex) { + public void sendSyncedVertex(Vertex vertex) { localSyncsSubject.onNext(vertex.getId()); } @@ -74,7 +74,7 @@ public void sendCommittedStateSync(long stateVersion, Object opaque) { } @Override - public void committedVertex(Vertex vertex) { + public void sendCommittedVertex(Vertex vertex) { committedVertices.onNext(vertex); } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/MissingParentException.java b/radixdlt/src/main/java/com/radixdlt/consensus/MissingParentException.java deleted file mode 100644 index c0c914a62..000000000 --- a/radixdlt/src/main/java/com/radixdlt/consensus/MissingParentException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.radixdlt.consensus; - -import com.radixdlt.crypto.Hash; -import java.util.Objects; - -/** - * Exception specifying that a vertex cannot be inserted because - * it's parent is missing from the current store. - */ -class MissingParentException extends RuntimeException { - MissingParentException(Hash parentId) { - super("Parent Vertex missing: " + Objects.requireNonNull(parentId)); - } -} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/RequiresSyncConsensusEvent.java b/radixdlt/src/main/java/com/radixdlt/consensus/RequiresSyncConsensusEvent.java index fdb496ca9..b902ff310 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/RequiresSyncConsensusEvent.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/RequiresSyncConsensusEvent.java @@ -17,8 +17,6 @@ package com.radixdlt.consensus; -import com.radixdlt.crypto.ECPublicKey; - /** * A consensus event which requires syncing to be effectively * processed @@ -36,10 +34,4 @@ public interface RequiresSyncConsensusEvent extends ConsensusEvent { * @return highest known committed QC of peer */ QuorumCertificate getCommittedQC(); - - /** - * Get the author of the event - * @return the author of the event - */ - ECPublicKey getAuthor(); } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/SyncEpochsRPCSender.java b/radixdlt/src/main/java/com/radixdlt/consensus/SyncEpochsRPCSender.java new file mode 100644 index 000000000..27865699e --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/SyncEpochsRPCSender.java @@ -0,0 +1,44 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus; + +import com.radixdlt.crypto.ECPublicKey; + +/** + * A sender of GetEpoch RPC requests/responses + */ +public interface SyncEpochsRPCSender { + + /** + * Send a request to a peer for proof of an epoch + * @param peer the peer to send to + * @param epoch the epoch to retrieve proof for + */ + void sendGetEpochRequest(ECPublicKey peer, long epoch); + + /** + * Send an epoch proof resposne to a peer + * + * TODO: currently just actually sending an ancestor but should contain + * TODO: proof as well + * + * @param peer the peer to send to + * @param ancestor the ancestor of the epoch + */ + void sendGetEpochResponse(ECPublicKey peer, VertexMetadata ancestor); +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCRx.java b/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCRx.java index 7cea56dac..6d50daec3 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCRx.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCRx.java @@ -17,7 +17,9 @@ package com.radixdlt.consensus; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; import io.reactivex.rxjava3.core.Observable; /** @@ -36,4 +38,10 @@ public interface SyncVerticesRPCRx { * @return a never-ending stream of responses */ Observable responses(); + + /** + * Retrieve a never-ending stream of error responses + * @return a never-ending stream of error responses + */ + Observable errorResponses(); } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCSender.java b/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCSender.java index 30d596505..24ca4d952 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCSender.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/SyncVerticesRPCSender.java @@ -18,7 +18,7 @@ package com.radixdlt.consensus; import com.google.common.collect.ImmutableList; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; @@ -44,4 +44,12 @@ public interface SyncVerticesRPCSender { * @param vertices the response data of vertices */ void sendGetVerticesResponse(GetVerticesRequest originalRequest, ImmutableList vertices); + + /** + * Send an RPC error response to a given request + * @param originalRequest the original request + * @param highestQC highestQC sync info + * @param highestCommittedQC highestCommittedQC sync info + */ + void sendGetVerticesErrorResponse(GetVerticesRequest originalRequest, QuorumCertificate highestQC, QuorumCertificate highestCommittedQC); } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreEventProcessor.java b/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreEventProcessor.java new file mode 100644 index 000000000..c06b51515 --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreEventProcessor.java @@ -0,0 +1,52 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus; + +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; + +/** + * Processor of vertex store events + */ +public interface VertexStoreEventProcessor { + + /** + * Process a get vertices request + * @param request the get vertices request + */ + void processGetVerticesRequest(GetVerticesRequest request); + + /** + * Process a get vertices error response + * @param response the get vertices error response + */ + void processGetVerticesErrorResponse(GetVerticesErrorResponse response); + + /** + * Process a get vertices response + * @param response the get vertices response + */ + void processGetVerticesResponse(GetVerticesResponse response); + + /** + * Process a committed state ync + * @param committedStateSync the committed state sync + */ + void processCommittedStateSync(CommittedStateSync committedStateSync); +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreFactory.java b/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreFactory.java index 55caf0e86..3163cb243 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreFactory.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/VertexStoreFactory.java @@ -17,6 +17,9 @@ package com.radixdlt.consensus; +import com.radixdlt.consensus.bft.VertexStore; +import com.radixdlt.middleware2.CommittedAtom; + /** * A Vertex Store factory */ @@ -26,7 +29,8 @@ public interface VertexStoreFactory { * Creates a new VertexStore given initial vertex and QC * @param genesisVertex the root vertex * @param genesisQC the root QC + * @param syncedStateComputer the underlying state computer * @return a new VertexStore */ - VertexStore create(Vertex genesisVertex, QuorumCertificate genesisQC); + VertexStore create(Vertex genesisVertex, QuorumCertificate genesisQC, SyncedStateComputer syncedStateComputer); } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/Vote.java b/radixdlt/src/main/java/com/radixdlt/consensus/Vote.java index aabbe100c..62791b2ef 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/Vote.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/Vote.java @@ -72,6 +72,7 @@ public long getEpoch() { return voteData.getProposed().getEpoch(); } + @Override public ECPublicKey getAuthor() { return author; } diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/bft/GetVerticesErrorResponse.java b/radixdlt/src/main/java/com/radixdlt/consensus/bft/GetVerticesErrorResponse.java new file mode 100644 index 000000000..bfb33e694 --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/bft/GetVerticesErrorResponse.java @@ -0,0 +1,60 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.bft; + +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.crypto.Hash; +import java.util.Objects; + +/** + * An error response to the GetVertices call + */ +public final class GetVerticesErrorResponse { + private final Hash vertexId; + private final Object opaque; + private final QuorumCertificate highestQC; + private final QuorumCertificate highestCommittedQC; + + public GetVerticesErrorResponse(Hash vertexId, QuorumCertificate highestQC, QuorumCertificate highestCommittedQC, Object opaque) { + this.vertexId = Objects.requireNonNull(vertexId); + this.highestQC = Objects.requireNonNull(highestQC); + this.highestCommittedQC = Objects.requireNonNull(highestCommittedQC); + this.opaque = opaque; + } + + public Hash getVertexId() { + return vertexId; + } + + public Object getOpaque() { + return opaque; + } + + public QuorumCertificate getHighestQC() { + return highestQC; + } + + public QuorumCertificate getHighestCommittedQC() { + return highestCommittedQC; + } + + @Override + public String toString() { + return String.format("%s{highQC=%s highCommittedQC=%s opaque=%s}", this.getClass().getSimpleName(), highestQC, highestCommittedQC, opaque); + } +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/GetVerticesResponse.java b/radixdlt/src/main/java/com/radixdlt/consensus/bft/GetVerticesResponse.java similarity index 95% rename from radixdlt/src/main/java/com/radixdlt/consensus/GetVerticesResponse.java rename to radixdlt/src/main/java/com/radixdlt/consensus/bft/GetVerticesResponse.java index 33c81ac75..13344ab4d 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/GetVerticesResponse.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/bft/GetVerticesResponse.java @@ -15,8 +15,9 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus; +package com.radixdlt.consensus.bft; +import com.radixdlt.consensus.Vertex; import com.radixdlt.crypto.Hash; import java.util.List; import java.util.Objects; diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/bft/MissingParentException.java b/radixdlt/src/main/java/com/radixdlt/consensus/bft/MissingParentException.java new file mode 100644 index 000000000..ede611ea7 --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/bft/MissingParentException.java @@ -0,0 +1,31 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.bft; + +import com.radixdlt.crypto.Hash; +import java.util.Objects; + +/** + * Exception specifying that a vertex cannot be inserted because + * it's parent is missing from the current store. + */ +class MissingParentException extends RuntimeException { + MissingParentException(Hash parentId) { + super("Parent Vertex missing: " + Objects.requireNonNull(parentId)); + } +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/VertexStore.java b/radixdlt/src/main/java/com/radixdlt/consensus/bft/VertexStore.java similarity index 77% rename from radixdlt/src/main/java/com/radixdlt/consensus/VertexStore.java rename to radixdlt/src/main/java/com/radixdlt/consensus/bft/VertexStore.java index 7c4f07187..a3461a598 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/VertexStore.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/bft/VertexStore.java @@ -1,23 +1,32 @@ /* - * (C) Copyright 2020 Radix DLT Ltd + * (C) Copyright 2020 Radix DLT Ltd * - * Radix DLT Ltd 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 + * Radix DLT Ltd 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 + * 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. + * 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 com.radixdlt.consensus; +package com.radixdlt.consensus.bft; import com.google.common.collect.ImmutableList; +import com.radixdlt.consensus.CommittedStateSync; +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.SyncVerticesRPCSender; +import com.radixdlt.consensus.SyncedStateComputer; +import com.radixdlt.consensus.Vertex; +import com.radixdlt.consensus.VertexInsertionException; +import com.radixdlt.consensus.VertexMetadata; +import com.radixdlt.consensus.VertexStoreEventProcessor; +import com.radixdlt.consensus.View; import com.radixdlt.counters.SystemCounters; import com.radixdlt.counters.SystemCounters.CounterType; import com.radixdlt.crypto.ECPublicKey; @@ -27,24 +36,23 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import javax.annotation.Nullable; +import javax.annotation.concurrent.NotThreadSafe; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * Manages the BFT Vertex chain. - * - * In general this class is NOT thread-safe except for getVertices() and getHighestQC(). */ -public final class VertexStore { +@NotThreadSafe +public final class VertexStore implements VertexStoreEventProcessor { private static final Logger log = LogManager.getLogger(); public interface GetVerticesRequest { @@ -53,24 +61,24 @@ public interface GetVerticesRequest { } public interface VertexStoreEventSender { - void syncedVertex(Vertex vertex); - void committedVertex(Vertex vertex); + // TODO: combine Synced and Committed + void sendSyncedVertex(Vertex vertex); + void sendCommittedVertex(Vertex vertex); void highQC(QuorumCertificate qc); } - private final VertexStoreEventSender vertexStoreEventSender; private final SyncVerticesRPCSender syncVerticesRPCSender; private final SyncedStateComputer syncedStateComputer; private final SystemCounters counters; - // These should never be empty - private final AtomicReference rootId = new AtomicReference<>(); - private final AtomicReference highestQC = new AtomicReference<>(); - private final AtomicReference highestCommittedQC = new AtomicReference<>(); + // These should never be null + private Hash rootId; + private QuorumCertificate highestQC; + private QuorumCertificate highestCommittedQC; - private final Map vertices = new ConcurrentHashMap<>(); - private final Map syncing = new ConcurrentHashMap<>(); + private final Map vertices = new HashMap<>(); + private final Map syncing = new HashMap<>(); public VertexStore( Vertex rootVertex, @@ -112,6 +120,10 @@ public VertexStore( this.rebuild(rootVertex, rootQC, rootQC, vertices); } + private Vertex getRoot() { + return this.vertices.get(this.rootId); + } + private void rebuild(Vertex rootVertex, QuorumCertificate rootQC, QuorumCertificate rootCommitQC, List vertices) { if (!rootQC.getProposed().getId().equals(rootVertex.getId())) { throw new IllegalStateException(String.format("rootQC=%s does not match rootVertex=%s", rootQC, rootVertex)); @@ -127,10 +139,10 @@ private void rebuild(Vertex rootVertex, QuorumCertificate rootQC, QuorumCertific } this.vertices.clear(); - this.rootId.set(rootVertex.getId()); - this.highestQC.set(rootQC); + this.rootId = rootVertex.getId(); + this.highestQC = rootQC; this.vertexStoreEventSender.highQC(rootQC); - this.highestCommittedQC.set(rootCommitQC); + this.highestCommittedQC = rootCommitQC; this.vertices.put(rootVertex.getId(), rootVertex); for (Vertex vertex : vertices) { @@ -154,6 +166,7 @@ private enum SyncStage { } private static class SyncState { + private final Hash localSyncId; private final QuorumCertificate qc; private final QuorumCertificate committedQC; private final VertexMetadata committedVertexMetadata; @@ -161,7 +174,9 @@ private static class SyncState { private SyncStage syncStage; private final LinkedList fetched = new LinkedList<>(); - SyncState(QuorumCertificate qc, QuorumCertificate committedQC, ECPublicKey author) { + SyncState(Hash localSyncId, QuorumCertificate qc, QuorumCertificate committedQC, ECPublicKey author) { + this.localSyncId = localSyncId; + if (committedQC.getView().equals(View.genesis())) { this.committedVertexMetadata = committedQC.getProposed(); } else { @@ -196,24 +211,30 @@ public String toString() { private boolean requiresCommittedStateSync(SyncState syncState) { final VertexMetadata committedMetadata = syncState.committedVertexMetadata; if (!vertices.containsKey(committedMetadata.getId())) { - View rootView = vertices.get(rootId.get()).getView(); + View rootView = this.getRoot().getView(); return rootView.compareTo(committedMetadata.getView()) < 0; } return false; } + @Override public void processGetVerticesRequest(GetVerticesRequest request) { // TODO: Handle nodes trying to DDOS this endpoint log.info("SYNC_VERTICES: Received GetVerticesRequest {}", request); ImmutableList fetched = this.getVertices(request.getVertexId(), request.getCount()); + if (fetched.isEmpty()) { + this.syncVerticesRPCSender.sendGetVerticesErrorResponse(request, this.getHighestQC(), this.getHighestCommittedQC()); + return; + } + log.info("SYNC_VERTICES: Sending Response {}", fetched); this.syncVerticesRPCSender.sendGetVerticesResponse(request, fetched); } private void rebuildAndSyncQC(SyncState syncState) { - log.info("SYNC_STATE: Rebuilding and syncing QC: sync={} curRoot={}", syncState, vertices.get(rootId.get())); + log.info("SYNC_STATE: Rebuilding and syncing QC: sync={} curRoot={}", syncState, this.getRoot()); // TODO: check if there are any vertices which haven't been local sync processed yet if (requiresCommittedStateSync(syncState)) { @@ -231,6 +252,7 @@ private void rebuildAndSyncQC(SyncState syncState) { } } + @Override public void processCommittedStateSync(CommittedStateSync committedStateSync) { log.info("SYNC_STATE: synced {}", committedStateSync); @@ -275,13 +297,16 @@ private void processVerticesResponseForQCSync(Hash syncTo, SyncState syncState, addQC(syncState.qc); } else { log.info("SYNC_VERTICES: Sending further GetVerticesRequest for qc={} fetched={} root={}", - syncState.qc, syncState.fetched.size(), vertices.get(rootId.get())); + syncState.qc, syncState.fetched.size(), this.getRoot()); syncVerticesRPCSender.sendGetVerticesRequest(nextVertexId, syncState.author, 1, syncTo); } } - public void processGetVerticesResponse(GetVerticesResponse response) { - log.info("SYNC_VERTICES: Received GetVerticesResponse {}", response); + @Override + public void processGetVerticesErrorResponse(GetVerticesErrorResponse response) { + // TODO: check response + + log.info("SYNC_VERTICES: Received GetVerticesErrorResponse {} ", response); final Hash syncTo = (Hash) response.getOpaque(); SyncState syncState = syncing.get(syncTo); @@ -289,11 +314,20 @@ public void processGetVerticesResponse(GetVerticesResponse response) { return; // sync requirements already satisfied by another sync } - if (response.getVertices().isEmpty()) { - log.info("GET_VERTICES failed: response was empty sync={}", syncState); - // failed - // TODO: retry - return; + // error response indicates that the node has moved on from last sync so try and sync to a new sync + this.startSync(syncTo, response.getHighestQC(), response.getHighestCommittedQC(), syncState.author); + } + + @Override + public void processGetVerticesResponse(GetVerticesResponse response) { + // TODO: check response + + log.info("SYNC_VERTICES: Received GetVerticesResponse {}", response); + + final Hash syncTo = (Hash) response.getOpaque(); + SyncState syncState = syncing.get(syncTo); + if (syncState == null) { + return; // sync requirements already satisfied by another sync } switch (syncState.syncStage) { @@ -309,19 +343,17 @@ public void processGetVerticesResponse(GetVerticesResponse response) { } private void doQCSync(SyncState syncState) { - final Hash vertexId = syncState.getQC().getProposed().getId(); syncState.setSyncStage(SyncStage.GET_QC_VERTICES); log.info("SYNC_VERTICES: QC: Sending initial GetVerticesRequest for sync={}", syncState); - syncVerticesRPCSender.sendGetVerticesRequest(vertexId, syncState.author, 1, vertexId); + syncVerticesRPCSender.sendGetVerticesRequest(syncState.qc.getProposed().getId(), syncState.author, 1, syncState.localSyncId); } private void doCommittedSync(SyncState syncState) { final Hash committedQCId = syncState.getCommittedQC().getProposed().getId(); - final Hash qcId = syncState.qc.getProposed().getId(); syncState.setSyncStage(SyncStage.GET_COMMITTED_VERTICES); log.info("SYNC_VERTICES: Committed: Sending initial GetVerticesRequest for sync={}", syncState); // Retrieve the 3 vertices preceding the committedQC so we can create a valid committed root - syncVerticesRPCSender.sendGetVerticesRequest(committedQCId, syncState.author, 3, qcId); + syncVerticesRPCSender.sendGetVerticesRequest(committedQCId, syncState.author, 3, syncState.localSyncId); } public void processLocalSync(Hash vertexId) { @@ -341,6 +373,10 @@ public void processLocalSync(Hash vertexId) { * @return true if already synced, false otherwise */ public boolean syncToQC(QuorumCertificate qc, QuorumCertificate committedQC, @Nullable ECPublicKey author) { + if (qc.getProposed().getView().compareTo(this.getRoot().getView()) < 0) { + return true; + } + if (addQC(qc)) { return true; } @@ -358,15 +394,19 @@ public boolean syncToQC(QuorumCertificate qc, QuorumCertificate committedQC, @Nu throw new IllegalStateException("Syncing required but author wasn't provided."); } - final SyncState syncState = new SyncState(qc, committedQC, author); + this.startSync(vertexId, qc, committedQC, author); + + return false; + } + + private void startSync(Hash vertexId, QuorumCertificate qc, QuorumCertificate committedQC, ECPublicKey author) { + final SyncState syncState = new SyncState(vertexId, qc, committedQC, author); syncing.put(vertexId, syncState); if (requiresCommittedStateSync(syncState)) { this.doCommittedSync(syncState); } else { this.doQCSync(syncState); } - - return false; } private boolean addQC(QuorumCertificate qc) { @@ -374,20 +414,19 @@ private boolean addQC(QuorumCertificate qc) { return false; } - if (highestQC.get().getView().compareTo(qc.getView()) < 0) { - highestQC.set(qc); + if (highestQC.getView().compareTo(qc.getView()) < 0) { + highestQC = qc; vertexStoreEventSender.highQC(qc); } qc.getCommitted().ifPresent(vertexMetadata -> { - QuorumCertificate highestCommitted = highestCommittedQC.get(); - Optional highest = highestCommitted.getCommitted(); - if (!highest.isPresent() && !highestCommitted.getView().isGenesis()) { - throw new IllegalStateException(String.format("Highest Committed does not have a commit: %s", highestCommitted)); + Optional highest = this.highestCommittedQC.getCommitted(); + if (!highest.isPresent() && !this.highestCommittedQC.getView().isGenesis()) { + throw new IllegalStateException(String.format("Highest Committed does not have a commit: %s", this.highestCommittedQC)); } if (!highest.isPresent() || highest.get().getView().compareTo(vertexMetadata.getView()) < 0) { - this.highestCommittedQC.set(qc); + this.highestCommittedQC = qc; } }); @@ -417,7 +456,7 @@ private VertexMetadata insertVertexInternal(Vertex vertex) throws VertexInsertio updateVertexStoreSize(); if (syncing.containsKey(vertexToUse.getId())) { - vertexStoreEventSender.syncedVertex(vertexToUse); + vertexStoreEventSender.sendSyncedVertex(vertexToUse); } return VertexMetadata.ofVertex(vertexToUse, isEndOfEpoch); @@ -436,7 +475,7 @@ public VertexMetadata insertVertex(Vertex vertex) throws VertexInsertionExceptio * @return the vertex if sucessful, otherwise an empty optional if vertex was already committed */ public Optional commitVertex(VertexMetadata commitMetadata) { - if (commitMetadata.getView().compareTo(vertices.get(rootId.get()).getView()) < 0) { + if (commitMetadata.getView().compareTo(this.getRoot().getView()) < 0) { return Optional.empty(); } @@ -447,7 +486,7 @@ public Optional commitVertex(VertexMetadata commitMetadata) { } final LinkedList path = new LinkedList<>(); Vertex vertex = tipVertex; - while (vertex != null && !rootId.get().equals(vertex.getId())) { + while (vertex != null && !rootId.equals(vertex.getId())) { path.addFirst(vertex); vertex = vertices.remove(vertex.getParentId()); } @@ -457,10 +496,10 @@ public Optional commitVertex(VertexMetadata commitMetadata) { this.counters.increment(CounterType.CONSENSUS_PROCESSED); syncedStateComputer.execute(committedAtom); - this.vertexStoreEventSender.committedVertex(committed); + this.vertexStoreEventSender.sendCommittedVertex(committed); } - rootId.set(commitMetadata.getId()); + rootId = commitMetadata.getId(); updateVertexStoreSize(); return Optional.of(tipVertex); @@ -470,7 +509,7 @@ public List getPathFromRoot(Hash vertexId) { final List path = new ArrayList<>(); Vertex vertex = vertices.get(vertexId); - while (vertex != null && !vertex.getId().equals(rootId.get())) { + while (vertex != null && !vertex.getId().equals(rootId)) { path.add(vertex); vertex = vertices.get(vertex.getParentId()); } @@ -483,7 +522,7 @@ public List getPathFromRoot(Hash vertexId) { * @return the highest committed qc */ public QuorumCertificate getHighestCommittedQC() { - return this.highestCommittedQC.get(); + return this.highestCommittedQC; } /** @@ -493,7 +532,7 @@ public QuorumCertificate getHighestCommittedQC() { * @return the highest quorum certificate */ public QuorumCertificate getHighestQC() { - return this.highestQC.get(); + return this.highestQC; } /** diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/epoch/GetEpochRequest.java b/radixdlt/src/main/java/com/radixdlt/consensus/epoch/GetEpochRequest.java new file mode 100644 index 000000000..e52194f7b --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/epoch/GetEpochRequest.java @@ -0,0 +1,42 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.epoch; + +import com.radixdlt.crypto.ECPublicKey; +import java.util.Objects; + +/** + * An RPC request to retrieve proof of an epoch + */ +public final class GetEpochRequest { + private final long epoch; + private final ECPublicKey sender; + + public GetEpochRequest(ECPublicKey sender, final long epoch) { + this.sender = Objects.requireNonNull(sender); + this.epoch = epoch; + } + + public long getEpoch() { + return epoch; + } + + public ECPublicKey getSender() { + return sender; + } +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/epoch/GetEpochResponse.java b/radixdlt/src/main/java/com/radixdlt/consensus/epoch/GetEpochResponse.java new file mode 100644 index 000000000..1a97bd090 --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/consensus/epoch/GetEpochResponse.java @@ -0,0 +1,46 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.epoch; + +import com.radixdlt.consensus.VertexMetadata; +import com.radixdlt.crypto.ECPublicKey; + +/** + * An RPC Response to a GetEpoch request + */ +public final class GetEpochResponse { + private final VertexMetadata epochAncestor; + private final ECPublicKey sender; + + public GetEpochResponse(ECPublicKey sender, VertexMetadata epochAncestor) { + this.epochAncestor = epochAncestor; + this.sender = sender; + } + + public ECPublicKey getSender() { + return sender; + } + + public VertexMetadata getEpochAncestor() { + return epochAncestor; + } + + public String toString() { + return String.format("%s{sender=%s ancestor=%s}", this.getClass().getSimpleName(), this.sender, this.epochAncestor); + } +} diff --git a/radixdlt/src/main/java/com/radixdlt/consensus/liveness/MempoolProposalGenerator.java b/radixdlt/src/main/java/com/radixdlt/consensus/liveness/MempoolProposalGenerator.java index 77414b6ee..cea245368 100644 --- a/radixdlt/src/main/java/com/radixdlt/consensus/liveness/MempoolProposalGenerator.java +++ b/radixdlt/src/main/java/com/radixdlt/consensus/liveness/MempoolProposalGenerator.java @@ -20,7 +20,7 @@ import com.radixdlt.identifiers.AID; import com.radixdlt.consensus.QuorumCertificate; import com.radixdlt.consensus.Vertex; -import com.radixdlt.consensus.VertexStore; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.View; import com.radixdlt.mempool.Mempool; import com.radixdlt.middleware2.ClientAtom; diff --git a/radixdlt/src/main/java/com/radixdlt/counters/SystemCounters.java b/radixdlt/src/main/java/com/radixdlt/counters/SystemCounters.java index 7b0221a14..b0868f2f8 100644 --- a/radixdlt/src/main/java/com/radixdlt/counters/SystemCounters.java +++ b/radixdlt/src/main/java/com/radixdlt/counters/SystemCounters.java @@ -39,7 +39,8 @@ enum CounterType { CONSENSUS_VIEW("consensus.view"), CONSENSUS_PROCESSED("consensus.processed"), // TODO: Move some CONSENSUS epoch related counters into epoch - EPOCHS_EPOCH("epochs.epoch"), + EPOCH_MANAGER_EPOCH("epoch_manager.epoch"), + EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS("epoch_manager.queued_consensus_events"), LEDGER_STATE_VERSION("ledger.state_version"), MEMPOOL_COUNT("mempool.count"), MEMPOOL_MAXCOUNT("mempool.maxcount"), diff --git a/radixdlt/src/main/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessage.java b/radixdlt/src/main/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessage.java new file mode 100644 index 000000000..15309dedb --- /dev/null +++ b/radixdlt/src/main/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessage.java @@ -0,0 +1,75 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.middleware2.network; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.crypto.Hash; +import com.radixdlt.serialization.DsonOutput; +import com.radixdlt.serialization.DsonOutput.Output; +import com.radixdlt.serialization.SerializerId2; +import java.util.Objects; +import org.radix.network.messaging.Message; + +@SerializerId2("message.consensus.vertices_error_response") +public class GetVerticesErrorResponseMessage extends Message { + @JsonProperty("vertexId") + @DsonOutput(Output.ALL) + private final Hash vertexId; + + @JsonProperty("highest_qc") + @DsonOutput(Output.ALL) + private final QuorumCertificate highestQC; + + @JsonProperty("highest_committed_qc") + @DsonOutput(Output.ALL) + private final QuorumCertificate highestCommittedQC; + + GetVerticesErrorResponseMessage() { + // Serializer only + super(0); + this.vertexId = null; + this.highestQC = null; + this.highestCommittedQC = null; + } + + GetVerticesErrorResponseMessage(int magic, Hash vertexId, QuorumCertificate highestQC, QuorumCertificate highestCommittedQC) { + super(magic); + this.vertexId = Objects.requireNonNull(vertexId); + this.highestQC = Objects.requireNonNull(highestQC); + this.highestCommittedQC = Objects.requireNonNull(highestCommittedQC); + } + + public Hash getVertexId() { + return vertexId; + } + + public QuorumCertificate getHighestQC() { + return highestQC; + } + + public QuorumCertificate getHighestCommittedQC() { + return highestCommittedQC; + } + + @Override + public String toString() { + return String.format("%s{vertexId=%s}", getClass().getSimpleName(), vertexId); + } + +} diff --git a/radixdlt/src/main/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetwork.java b/radixdlt/src/main/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetwork.java index 55610c2a0..7eb52674f 100644 --- a/radixdlt/src/main/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetwork.java +++ b/radixdlt/src/main/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetwork.java @@ -21,11 +21,13 @@ import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.inject.name.Named; -import com.radixdlt.consensus.GetVerticesResponse; +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; import com.radixdlt.consensus.SyncVerticesRPCRx; import com.radixdlt.consensus.SyncVerticesRPCSender; import com.radixdlt.consensus.Vertex; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; import com.radixdlt.network.addressbook.AddressBook; @@ -92,6 +94,19 @@ public void sendGetVerticesResponse(GetVerticesRequest originalRequest, Immutabl this.messageCentral.send(peer, response); } + @Override + public void sendGetVerticesErrorResponse(GetVerticesRequest originalRequest, QuorumCertificate highestQC, QuorumCertificate highestCommittedQC) { + MessageCentralGetVerticesRequest messageCentralGetVerticesRequest = (MessageCentralGetVerticesRequest) originalRequest; + GetVerticesErrorResponseMessage response = new GetVerticesErrorResponseMessage( + this.magic, + messageCentralGetVerticesRequest.getVertexId(), + highestQC, + highestCommittedQC + ); + Peer peer = messageCentralGetVerticesRequest.getRequestor(); + this.messageCentral.send(peer, response); + } + @Override public Observable requests() { return Observable.create(emitter -> { @@ -121,6 +136,28 @@ public Observable responses() { }); } + @Override + public Observable errorResponses() { + return Observable.create(emitter -> { + MessageListener listener = (src, msg) -> { + Object opaque = opaqueCache.getIfPresent(msg.getVertexId()); + if (opaque == null) { + return; // TODO: send error? + } + + GetVerticesErrorResponse response = new GetVerticesErrorResponse( + msg.getVertexId(), + msg.getHighestQC(), + msg.getHighestCommittedQC(), + opaque + ); + emitter.onNext(response); + }; + this.messageCentral.addListener(GetVerticesErrorResponseMessage.class, listener); + emitter.setCancellable(() -> this.messageCentral.removeListener(listener)); + }); + } + /** * An RPC request to retrieve a given vertex */ diff --git a/radixdlt/src/main/java/com/radixdlt/middleware2/network/TestEventCoordinatorNetwork.java b/radixdlt/src/main/java/com/radixdlt/middleware2/network/TestEventCoordinatorNetwork.java index 45a57914a..3139d5050 100644 --- a/radixdlt/src/main/java/com/radixdlt/middleware2/network/TestEventCoordinatorNetwork.java +++ b/radixdlt/src/main/java/com/radixdlt/middleware2/network/TestEventCoordinatorNetwork.java @@ -21,13 +21,15 @@ import com.radixdlt.consensus.ConsensusEvent; import com.radixdlt.consensus.EventCoordinatorNetworkRx; import com.radixdlt.consensus.BFTEventSender; -import com.radixdlt.consensus.GetVerticesResponse; +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; import com.radixdlt.consensus.NewView; import com.radixdlt.consensus.Proposal; import com.radixdlt.consensus.SyncVerticesRPCRx; import com.radixdlt.consensus.SyncVerticesRPCSender; import com.radixdlt.consensus.Vertex; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.consensus.Vote; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; @@ -245,6 +247,16 @@ public void sendGetVerticesResponse(GetVerticesRequest originalRequest, Immutabl receivedMessages.onNext(MessageInTransit.newMessage(vertexResponse, thisNode, request.requestor)); } + @Override + public void sendGetVerticesErrorResponse(GetVerticesRequest originalRequest, QuorumCertificate highestQC, + QuorumCertificate highestCommittedQC) { + + SimulatedVerticesRequest request = (SimulatedVerticesRequest) originalRequest; + Object opaque = receivers.computeIfAbsent(request.requestor, SimulatedNetworkImpl::new).opaqueMap.get(request.vertexId); + GetVerticesErrorResponse vertexResponse = new GetVerticesErrorResponse(request.vertexId, highestQC, highestCommittedQC, opaque); + receivedMessages.onNext(MessageInTransit.newMessage(vertexResponse, thisNode, request.requestor)); + } + @Override public Observable consensusEvents() { return myMessages.ofType(ConsensusEvent.class); @@ -259,6 +271,11 @@ public Observable requests() { public Observable responses() { return myMessages.ofType(GetVerticesResponse.class); } + + @Override + public Observable errorResponses() { + return myMessages.ofType(GetVerticesErrorResponse.class); + } } public SimulatedNetworkReceiver getNetworkRx(ECPublicKey forNode) { diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventPreprocessorTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventPreprocessorTest.java index 099f3327d..0f5805a42 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventPreprocessorTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventPreprocessorTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.liveness.Pacemaker; import com.radixdlt.consensus.liveness.ProposerElection; import com.radixdlt.crypto.ECKeyPair; diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventReducerTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventReducerTest.java index ba6993851..1792ade36 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventReducerTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/BFTEventReducerTest.java @@ -18,6 +18,7 @@ package com.radixdlt.consensus; import com.google.common.collect.Lists; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.liveness.ProposalGenerator; import com.radixdlt.identifiers.AID; import com.radixdlt.consensus.liveness.Pacemaker; diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/ConsensusRunnerTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/ConsensusRunnerTest.java index ff9785c45..1a9f7792c 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/ConsensusRunnerTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/ConsensusRunnerTest.java @@ -24,7 +24,7 @@ import static org.mockito.Mockito.when; import com.radixdlt.consensus.ConsensusRunner.Event; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.consensus.liveness.PacemakerRx; import com.radixdlt.crypto.Hash; import io.reactivex.rxjava3.core.Observable; @@ -56,6 +56,7 @@ public void when_events_get_emitted__then_event_coordinator_should_be_called() { GetVerticesRequest request = mock(GetVerticesRequest.class); when(syncVerticesRPCRx.requests()).thenReturn(Observable.just(request).concatWith(Observable.never())); when(syncVerticesRPCRx.responses()).thenReturn(Observable.never()); + when(syncVerticesRPCRx.errorResponses()).thenReturn(Observable.never()); VertexStoreEventsRx vertexStoreEventsRx = mock(VertexStoreEventsRx.class); Hash id = mock(Hash.class); @@ -65,7 +66,8 @@ public void when_events_get_emitted__then_event_coordinator_should_be_called() { CommittedStateSync stateSync = mock(CommittedStateSync.class); when(committedStateSyncRx.committedStateSyncs()).thenReturn(Observable.just(stateSync).concatWith(Observable.never())); - ConsensusRunner consensusRunner = new ConsensusRunner(epochChangeRx, + ConsensusRunner consensusRunner = new ConsensusRunner( + epochChangeRx, networkRx, pacemakerRx, vertexStoreEventsRx, diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/EmptySyncEpochsRPCSenderTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/EmptySyncEpochsRPCSenderTest.java new file mode 100644 index 000000000..f8f50a06a --- /dev/null +++ b/radixdlt/src/test/java/com/radixdlt/consensus/EmptySyncEpochsRPCSenderTest.java @@ -0,0 +1,32 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus; + +import static org.mockito.Mockito.mock; + +import com.radixdlt.crypto.ECPublicKey; +import org.junit.Test; + +public class EmptySyncEpochsRPCSenderTest { + @Test + public void when_send_request_and_response__then_no_exception_occurs() { + EmptySyncEpochsRPCSender.INSTANCE.sendGetEpochRequest(mock(ECPublicKey.class), 12345L); + EmptySyncEpochsRPCSender.INSTANCE.sendGetEpochResponse(mock(ECPublicKey.class), mock(VertexMetadata.class)); + } + +} \ No newline at end of file diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/EpochManagerTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/EpochManagerTest.java index 1656f7534..ca1e8954b 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/EpochManagerTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/EpochManagerTest.java @@ -17,41 +17,93 @@ package com.radixdlt.consensus; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableSet; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.VertexStore; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.GetVerticesResponse; +import com.radixdlt.consensus.epoch.GetEpochRequest; +import com.radixdlt.consensus.epoch.GetEpochResponse; import com.radixdlt.consensus.liveness.Pacemaker; import com.radixdlt.consensus.liveness.ProposerElection; import com.radixdlt.consensus.liveness.ScheduledTimeoutSender; import com.radixdlt.consensus.validators.Validator; import com.radixdlt.consensus.validators.ValidatorSet; import com.radixdlt.counters.SystemCounters; +import com.radixdlt.counters.SystemCounters.CounterType; import com.radixdlt.crypto.ECKeyPair; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; -import com.radixdlt.mempool.Mempool; +import com.radixdlt.middleware2.CommittedAtom; +import org.junit.Before; import org.junit.Test; public class EpochManagerTest { - @Test - public void when_no_epoch_change__then_processing_events_should_not_fail() { - ECKeyPair keyPair = mock(ECKeyPair.class); - EpochManager epochManager = new EpochManager( - mock(Mempool.class), - mock(BFTEventSender.class), + private ECPublicKey publicKey; + private EpochManager epochManager; + private SyncEpochsRPCSender syncEpochsRPCSender; + private VertexStore vertexStore; + private BFTFactory bftFactory; + private Pacemaker pacemaker; + private SystemCounters systemCounters; + private SyncedStateComputer syncedStateComputer; + + @Before + public void setup() { + ECKeyPair keyPair = ECKeyPair.generateNew(); + this.publicKey = keyPair.getPublicKey(); + + this.syncEpochsRPCSender = mock(SyncEpochsRPCSender.class); + + this.vertexStore = mock(VertexStore.class); + VertexStoreFactory vertexStoreFactory = mock(VertexStoreFactory.class); + when(vertexStoreFactory.create(any(), any(), any())).thenReturn(this.vertexStore); + this.pacemaker = mock(Pacemaker.class); + + this.bftFactory = mock(BFTFactory.class); + + this.systemCounters = SystemCounters.getInstance(); + this.syncedStateComputer = mock(SyncedStateComputer.class); + + this.epochManager = new EpochManager( + this.syncedStateComputer, + this.syncEpochsRPCSender, mock(ScheduledTimeoutSender.class), - timeoutSender -> mock(Pacemaker.class), - mock(VertexStoreFactory.class), + timeoutSender -> this.pacemaker, + vertexStoreFactory, proposers -> mock(ProposerElection.class), - mock(Hasher.class), - keyPair, - mock(SystemCounters.class) + this.bftFactory, + this.publicKey, + this.systemCounters ); + } + + @Test + public void when_next_epoch_does_not_contain_self__then_should_not_emit_any_consensus_events() { + EpochChange epochChange = mock(EpochChange.class); + ValidatorSet validatorSet = mock(ValidatorSet.class); + when(validatorSet.containsKey(eq(publicKey))).thenReturn(false); + when(epochChange.getValidatorSet()).thenReturn(validatorSet); + VertexMetadata vertexMetadata = mock(VertexMetadata.class); + when(epochChange.getAncestor()).thenReturn(vertexMetadata); + epochManager.processEpochChange(epochChange); + + verify(bftFactory, never()).create(any(), any(), any(), any()); + verify(syncEpochsRPCSender, never()).sendGetEpochRequest(any(), anyLong()); + } + + @Test + public void when_no_epoch_change__then_processing_events_should_not_fail() { epochManager.processLocalTimeout(mock(LocalTimeout.class)); epochManager.processLocalSync(mock(Hash.class)); epochManager.processGetVerticesRequest(mock(GetVerticesRequest.class)); @@ -62,28 +114,93 @@ public void when_no_epoch_change__then_processing_events_should_not_fail() { epochManager.processConsensusEvent(mock(Vote.class)); } + @Test + public void when_receive_next_epoch_then_epoch_request__then_should_return_current_ancestor() { + VertexMetadata ancestor = VertexMetadata.ofGenesisAncestor(); + ValidatorSet validatorSet = mock(ValidatorSet.class); + epochManager.processEpochChange(new EpochChange(ancestor, validatorSet)); + ECPublicKey sender = mock(ECPublicKey.class); + epochManager.processGetEpochRequest(new GetEpochRequest(sender, ancestor.getEpoch() + 1)); + verify(syncEpochsRPCSender, times(1)).sendGetEpochResponse(eq(sender), eq(ancestor)); + } + + @Test + public void when_receive_epoch_response__then_should_sync_state_computer() { + GetEpochResponse response = mock(GetEpochResponse.class); + when(response.getEpochAncestor()).thenReturn(VertexMetadata.ofGenesisAncestor()); + epochManager.processGetEpochResponse(response); + verify(syncedStateComputer, times(1)).syncTo(eq(VertexMetadata.ofGenesisAncestor()), any(), any()); + } + + @Test + public void when_receive_next_epoch_events_and_then_epoch_change_and_part_of_validator_set__then_should_execute_queued_epoch_events() { + Validator authorValidator = mock(Validator.class); + ECPublicKey author = mock(ECPublicKey.class); + when(authorValidator.nodeKey()).thenReturn(author); + + when(pacemaker.getCurrentView()).thenReturn(View.genesis()); + when(vertexStore.getHighestQC()).thenReturn(mock(QuorumCertificate.class)); + when(vertexStore.syncToQC(any(), any(), any())).thenReturn(true); + + VertexMetadata ancestor = VertexMetadata.ofGenesisAncestor(); + + Proposal proposal = mock(Proposal.class); + Vertex vertex = mock(Vertex.class); + when(vertex.getView()).thenReturn(View.of(1)); + when(proposal.getEpoch()).thenReturn(ancestor.getEpoch() + 1); + when(proposal.getVertex()).thenReturn(vertex); + when(proposal.getAuthor()).thenReturn(author); + epochManager.processConsensusEvent(proposal); + + assertThat(systemCounters.get(CounterType.EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS)).isEqualTo(1); + + BFTEventProcessor eventProcessor = mock(BFTEventProcessor.class); + when(bftFactory.create(any(), any(), any(), any())).thenReturn(eventProcessor); + + Validator validator = mock(Validator.class); + when(validator.nodeKey()).thenReturn(mock(ECPublicKey.class)); + ValidatorSet validatorSet = mock(ValidatorSet.class); + when(validatorSet.containsKey(any())).thenReturn(true); + when(validatorSet.getValidators()).thenReturn(ImmutableSet.of(validator, authorValidator)); + epochManager.processEpochChange(new EpochChange(ancestor, validatorSet)); + + assertThat(systemCounters.get(CounterType.EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS)).isEqualTo(0); + verify(eventProcessor, times(1)).processProposal(eq(proposal)); + } + + @Test + public void when_receive_next_epoch_events_and_then_epoch_change_and_not_part_of_validator_set__then_queued_events_should_be_cleared() { + Validator authorValidator = mock(Validator.class); + ECPublicKey author = mock(ECPublicKey.class); + when(authorValidator.nodeKey()).thenReturn(author); + + VertexMetadata ancestor = VertexMetadata.ofGenesisAncestor(); + + Proposal proposal = mock(Proposal.class); + when(proposal.getEpoch()).thenReturn(ancestor.getEpoch() + 1); + epochManager.processConsensusEvent(proposal); + assertThat(systemCounters.get(CounterType.EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS)).isEqualTo(1); + + Validator validator = mock(Validator.class); + when(validator.nodeKey()).thenReturn(mock(ECPublicKey.class)); + ValidatorSet validatorSet = mock(ValidatorSet.class); + when(validatorSet.containsKey(any())).thenReturn(false); + epochManager.processEpochChange(new EpochChange(ancestor, validatorSet)); + + assertThat(systemCounters.get(CounterType.EPOCH_MANAGER_QUEUED_CONSENSUS_EVENTS)).isEqualTo(0); + } + @Test public void when_next_epoch__then_get_vertices_rpc_should_be_forwarded_to_vertex_store() { - ECKeyPair keyPair = mock(ECKeyPair.class); - when(keyPair.getPublicKey()).thenReturn(mock(ECPublicKey.class)); - VertexStore vertexStore = mock(VertexStore.class); when(vertexStore.getHighestQC()).thenReturn(mock(QuorumCertificate.class)); - EpochManager epochManager = new EpochManager( - mock(Mempool.class), - mock(BFTEventSender.class), - mock(ScheduledTimeoutSender.class), - timeoutSender -> mock(Pacemaker.class), - (v, qc) -> vertexStore, - proposers -> mock(ProposerElection.class), - mock(Hasher.class), - keyPair, - mock(SystemCounters.class) - ); + BFTEventProcessor eventProcessor = mock(BFTEventProcessor.class); + when(bftFactory.create(any(), any(), any(), any())).thenReturn(eventProcessor); Validator validator = mock(Validator.class); when(validator.nodeKey()).thenReturn(mock(ECPublicKey.class)); ValidatorSet validatorSet = mock(ValidatorSet.class); + when(validatorSet.containsKey(any())).thenReturn(true); when(validatorSet.getValidators()).thenReturn(ImmutableSet.of(validator)); epochManager.processEpochChange(new EpochChange(VertexMetadata.ofGenesisAncestor(), validatorSet)); @@ -95,6 +212,10 @@ public void when_next_epoch__then_get_vertices_rpc_should_be_forwarded_to_vertex epochManager.processGetVerticesResponse(getVerticesResponse); verify(vertexStore, times(1)).processGetVerticesResponse(eq(getVerticesResponse)); + GetVerticesErrorResponse getVerticesErrorResponse = mock(GetVerticesErrorResponse.class); + epochManager.processGetVerticesErrorResponse(getVerticesErrorResponse); + verify(vertexStore, times(1)).processGetVerticesErrorResponse(eq(getVerticesErrorResponse)); + CommittedStateSync committedStateSync = mock(CommittedStateSync.class); epochManager.processCommittedStateSync(committedStateSync); verify(vertexStore, times(1)).processCommittedStateSync(eq(committedStateSync)); diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/InternalMessagePasserTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/InternalMessagePasserTest.java index 4abd5b207..dfb30e61d 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/InternalMessagePasserTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/InternalMessagePasserTest.java @@ -32,7 +32,7 @@ public void when_send_sync_event__then_should_receive_it() { Hash hash = mock(Hash.class); Vertex vertex = mock(Vertex.class); when(vertex.getId()).thenReturn(hash); - internalMessagePasser.syncedVertex(vertex); + internalMessagePasser.sendSyncedVertex(vertex); testObserver.awaitCount(1); testObserver.assertValue(hash); testObserver.assertNotComplete(); @@ -43,7 +43,7 @@ public void when_send_committed_vertex_event__then_should_receive_it() { InternalMessagePasser internalMessagePasser = new InternalMessagePasser(); TestObserver testObserver = internalMessagePasser.committedVertices().test(); Vertex vertex = mock(Vertex.class); - internalMessagePasser.committedVertex(vertex); + internalMessagePasser.sendCommittedVertex(vertex); testObserver.awaitCount(1); testObserver.assertValue(vertex); testObserver.assertNotComplete(); diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/bft/GetVerticesErrorResponseTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/bft/GetVerticesErrorResponseTest.java new file mode 100644 index 000000000..7ac9ded90 --- /dev/null +++ b/radixdlt/src/test/java/com/radixdlt/consensus/bft/GetVerticesErrorResponseTest.java @@ -0,0 +1,57 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.bft; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.crypto.Hash; +import org.junit.Before; +import org.junit.Test; + +public class GetVerticesErrorResponseTest { + private Hash vertexId; + private Object opaque; + private QuorumCertificate highestQC; + private QuorumCertificate highestCommittedQC; + private GetVerticesErrorResponse response; + + @Before + public void setUp() { + this.vertexId = mock(Hash.class); + this.opaque = mock(Object.class); + this.highestQC = mock(QuorumCertificate.class); + this.highestCommittedQC = mock(QuorumCertificate.class); + this.response = new GetVerticesErrorResponse(this.vertexId, this.highestQC, this.highestCommittedQC, this.opaque); + } + + @Test + public void testGetters() { + assertThat(this.response.getVertexId()).isEqualTo(this.vertexId); + assertThat(this.response.getOpaque()).isEqualTo(this.opaque); + assertThat(this.response.getHighestQC()).isEqualTo(this.highestQC); + assertThat(this.response.getHighestCommittedQC()).isEqualTo(this.highestCommittedQC); + } + + @Test + public void testToString() { + assertThat(this.response.toString()).isNotNull(); + } + +} \ No newline at end of file diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/VertexStoreTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/bft/VertexStoreTest.java similarity index 93% rename from radixdlt/src/test/java/com/radixdlt/consensus/VertexStoreTest.java rename to radixdlt/src/test/java/com/radixdlt/consensus/bft/VertexStoreTest.java index ee2bb87ba..743c765f2 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/VertexStoreTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/bft/VertexStoreTest.java @@ -6,7 +6,7 @@ * compliance with the License. You may obtain a copy of the * License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 @@ -15,7 +15,7 @@ * language governing permissions and limitations under the License. */ -package com.radixdlt.consensus; +package com.radixdlt.consensus.bft; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -31,8 +31,17 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; -import com.radixdlt.consensus.VertexStore.VertexStoreEventSender; +import com.radixdlt.consensus.CommittedStateSync; +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.SyncVerticesRPCSender; +import com.radixdlt.consensus.SyncedStateComputer; +import com.radixdlt.consensus.Vertex; +import com.radixdlt.consensus.VertexInsertionException; +import com.radixdlt.consensus.VertexMetadata; +import com.radixdlt.consensus.View; +import com.radixdlt.consensus.VoteData; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.VertexStoreEventSender; import com.radixdlt.counters.SystemCounters; import com.radixdlt.crypto.ECDSASignatures; import com.radixdlt.crypto.ECPublicKey; @@ -166,7 +175,7 @@ public void when_vertex_retriever_succeeds__then_vertex_is_inserted() { GetVerticesResponse getVerticesResponse = new GetVerticesResponse(vertex.getId(), Collections.singletonList(vertex), opaque.get()); vertexStore.processGetVerticesResponse(getVerticesResponse); - verify(vertexStoreEventSender, times(1)).syncedVertex(eq(vertex)); + verify(vertexStoreEventSender, times(1)).sendSyncedVertex(eq(vertex)); assertThat(vertexStore.getHighestQC()).isEqualTo(qc); } @@ -222,7 +231,7 @@ public void when_insert_vertex__then_it_should_not_be_committed_or_stored_in_eng Vertex nextVertex = Vertex.createVertex(rootQC, View.of(1), null); vertexStore.insertVertex(nextVertex); - verify(vertexStoreEventSender, never()).committedVertex(any()); + verify(vertexStoreEventSender, never()).sendCommittedVertex(any()); verify(syncedStateComputer, times(0)).execute(any()); // not stored } @@ -237,7 +246,7 @@ public void when_insert_and_commit_vertex__then_it_should_be_committed_and_store assertThat(vertexStore.commitVertex(vertexMetadata)).hasValue(nextVertex); verify(vertexStoreEventSender, times(1)) - .committedVertex(eq(nextVertex)); + .sendCommittedVertex(eq(nextVertex)); verify(syncedStateComputer, times(1)) .execute(argThat(a -> a.getClientAtom().equals(clientAtom))); // next atom stored } @@ -260,7 +269,7 @@ public void when_insert_and_commit_vertex__then_committed_vertex_should_emit_and VertexMetadata vertexMetadata = VertexMetadata.ofVertex(vertex, false); assertThat(vertexStore.commitVertex(vertexMetadata)).hasValue(vertex); - verify(vertexStoreEventSender, times(1)).committedVertex(eq(vertex)); + verify(vertexStoreEventSender, times(1)).sendCommittedVertex(eq(vertex)); assertThat(vertexStore.getSize()).isEqualTo(1); } @@ -278,8 +287,8 @@ public void when_insert_and_commit_vertex_2x__then_committed_vertex_should_emit_ VertexMetadata vertexMetadata2 = VertexMetadata.ofVertex(nextVertex2, false); vertexStore.commitVertex(vertexMetadata2); - verify(vertexStoreEventSender, times(1)).committedVertex(eq(nextVertex1)); - verify(vertexStoreEventSender, times(1)).committedVertex(eq(nextVertex2)); + verify(vertexStoreEventSender, times(1)).sendCommittedVertex(eq(nextVertex1)); + verify(vertexStoreEventSender, times(1)).sendCommittedVertex(eq(nextVertex2)); assertThat(vertexStore.getSize()).isEqualTo(1); assertThat(vertexStore.getSize()).isEqualTo(1); } @@ -297,8 +306,8 @@ public void when_insert_two_and_commit_vertex__then_two_committed_vertices_shoul VertexMetadata vertexMetadata2 = VertexMetadata.ofVertex(nextVertex2, false); vertexStore.commitVertex(vertexMetadata2); - verify(vertexStoreEventSender, times(1)).committedVertex(eq(nextVertex1)); - verify(vertexStoreEventSender, times(1)).committedVertex(eq(nextVertex2)); + verify(vertexStoreEventSender, times(1)).sendCommittedVertex(eq(nextVertex1)); + verify(vertexStoreEventSender, times(1)).sendCommittedVertex(eq(nextVertex2)); assertThat(vertexStore.getSize()).isEqualTo(1); } @@ -310,7 +319,7 @@ public void when_sync_to_qc_which_doesnt_exist_and_vertex_is_inserted_later__the assertThat(vertexStore.syncToQC(qc, vertexStore.getHighestCommittedQC(), mock(ECPublicKey.class))).isFalse(); vertexStore.insertVertex(vertex); - verify(vertexStoreEventSender, times(1)).syncedVertex(eq(vertex)); + verify(vertexStoreEventSender, times(1)).sendSyncedVertex(eq(vertex)); } @Test @@ -449,7 +458,7 @@ public void when_request_for_qc_sync_and_receive_response__then_should_update() assertThat(vertexStore.getHighestQC()).isEqualTo(vertex5.getQC()); - verify(vertexStoreEventSender, times(1)).syncedVertex(eq(vertex4)); + verify(vertexStoreEventSender, times(1)).sendSyncedVertex(eq(vertex4)); } @Test diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/epoch/GetEpochRequestTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/epoch/GetEpochRequestTest.java new file mode 100644 index 000000000..f76ef1dd5 --- /dev/null +++ b/radixdlt/src/test/java/com/radixdlt/consensus/epoch/GetEpochRequestTest.java @@ -0,0 +1,49 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.epoch; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +import com.radixdlt.crypto.ECPublicKey; +import org.junit.Before; +import org.junit.Test; + +public class GetEpochRequestTest { + private ECPublicKey sender; + private long epoch; + private GetEpochRequest request; + + @Before + public void setUp() { + this.sender = mock(ECPublicKey.class); + this.epoch = 12345; + this.request = new GetEpochRequest(this.sender, this.epoch); + } + + @Test + public void testGetters() { + assertThat(this.request.getEpoch()).isEqualTo(epoch); + assertThat(this.request.getSender()).isEqualTo(this.sender); + } + + @Test + public void testToString() { + assertThat(this.request.toString()).isNotNull(); + } +} \ No newline at end of file diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/epoch/GetEpochResponseTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/epoch/GetEpochResponseTest.java new file mode 100644 index 000000000..e3d3a079c --- /dev/null +++ b/radixdlt/src/test/java/com/radixdlt/consensus/epoch/GetEpochResponseTest.java @@ -0,0 +1,51 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.consensus.epoch; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +import com.radixdlt.consensus.VertexMetadata; +import com.radixdlt.crypto.ECPublicKey; +import org.junit.Before; +import org.junit.Test; + +public class GetEpochResponseTest { + private ECPublicKey sender; + private VertexMetadata ancestor; + private GetEpochResponse response; + + @Before + public void setUp() { + this.sender = mock(ECPublicKey.class); + this.ancestor = mock(VertexMetadata.class); + this.response = new GetEpochResponse(this.sender, this.ancestor); + } + + @Test + public void testGetters() { + assertThat(this.response.getEpochAncestor()).isEqualTo(this.ancestor); + assertThat(this.response.getSender()).isEqualTo(this.sender); + } + + @Test + public void testToString() { + assertThat(this.response.toString()).isNotNull(); + } + +} \ No newline at end of file diff --git a/radixdlt/src/test/java/com/radixdlt/consensus/liveness/MempoolProposalGeneratorTest.java b/radixdlt/src/test/java/com/radixdlt/consensus/liveness/MempoolProposalGeneratorTest.java index fa6784662..59bc56517 100644 --- a/radixdlt/src/test/java/com/radixdlt/consensus/liveness/MempoolProposalGeneratorTest.java +++ b/radixdlt/src/test/java/com/radixdlt/consensus/liveness/MempoolProposalGeneratorTest.java @@ -27,7 +27,7 @@ import com.radixdlt.consensus.QuorumCertificate; import com.radixdlt.consensus.Vertex; import com.radixdlt.consensus.VertexMetadata; -import com.radixdlt.consensus.VertexStore; +import com.radixdlt.consensus.bft.VertexStore; import com.radixdlt.consensus.View; import com.radixdlt.mempool.Mempool; import com.radixdlt.middleware2.ClientAtom; diff --git a/radixdlt/src/test/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessageSerializeTest.java b/radixdlt/src/test/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessageSerializeTest.java new file mode 100644 index 000000000..743d3982a --- /dev/null +++ b/radixdlt/src/test/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessageSerializeTest.java @@ -0,0 +1,38 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.middleware2.network; + +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.Vertex; +import com.radixdlt.crypto.Hash; +import org.radix.serialization.SerializeMessageObject; + +public class GetVerticesErrorResponseMessageSerializeTest extends SerializeMessageObject { + public GetVerticesErrorResponseMessageSerializeTest() { + super(GetVerticesErrorResponseMessage.class, GetVerticesErrorResponseMessageSerializeTest::get); + } + + private static GetVerticesErrorResponseMessage get() { + return new GetVerticesErrorResponseMessage( + 12345, + Hash.random(), + QuorumCertificate.ofGenesis(Vertex.createGenesis()), + QuorumCertificate.ofGenesis(Vertex.createGenesis()) + ); + } +} diff --git a/radixdlt/src/test/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessageTest.java b/radixdlt/src/test/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessageTest.java new file mode 100644 index 000000000..0c46afd02 --- /dev/null +++ b/radixdlt/src/test/java/com/radixdlt/middleware2/network/GetVerticesErrorResponseMessageTest.java @@ -0,0 +1,38 @@ +/* + * (C) Copyright 2020 Radix DLT Ltd + * + * Radix DLT Ltd 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 com.radixdlt.middleware2.network; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.Vertex; +import com.radixdlt.crypto.Hash; +import org.junit.Test; + +public class GetVerticesErrorResponseMessageTest { + @Test + public void sensibleToString() { + Hash vertexId = Hash.random(); + QuorumCertificate qc = QuorumCertificate.ofGenesis(Vertex.createGenesis()); + GetVerticesErrorResponseMessage msg1 = new GetVerticesErrorResponseMessage(0, vertexId, qc, qc); + String s1 = msg1.toString(); + assertThat(s1, containsString(GetVerticesErrorResponseMessage.class.getSimpleName())); + assertThat(s1, containsString(vertexId.toString())); + } +} \ No newline at end of file diff --git a/radixdlt/src/test/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetworkTest.java b/radixdlt/src/test/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetworkTest.java index 8a2a0f75c..fd0c877f7 100644 --- a/radixdlt/src/test/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetworkTest.java +++ b/radixdlt/src/test/java/com/radixdlt/middleware2/network/MessageCentralSyncVerticesRPCNetworkTest.java @@ -27,9 +27,11 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; -import com.radixdlt.consensus.GetVerticesResponse; +import com.radixdlt.consensus.QuorumCertificate; +import com.radixdlt.consensus.bft.GetVerticesErrorResponse; +import com.radixdlt.consensus.bft.GetVerticesResponse; import com.radixdlt.consensus.Vertex; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.crypto.ECKeyPair; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.crypto.Hash; @@ -108,6 +110,39 @@ public void when_send_request_and_receive_response__then_should_receive_same_opa testObserver.assertValue(resp -> resp.getOpaque().equals(opaque)); } + @Test + public void when_send_request_and_receive_error_response__then_should_receive_same_opaque() { + Hash id = mock(Hash.class); + ECPublicKey node = mock(ECPublicKey.class); + when(node.euid()).thenReturn(EUID.ONE); + Peer peer = mock(Peer.class); + when(addressBook.peer(eq(EUID.ONE))).thenReturn(Optional.of(peer)); + int count = 1; + Object opaque = mock(Object.class); + network.sendGetVerticesRequest(id, node, count, opaque); + verify(messageCentral, times(1)).send(eq(peer), any(GetVerticesRequestMessage.class)); + + AtomicReference> listener = new AtomicReference<>(); + + doAnswer(invocation -> { + listener.set(invocation.getArgument(1)); + return null; + }).when(messageCentral).addListener(eq(GetVerticesErrorResponseMessage.class), any()); + + TestObserver testObserver = network.errorResponses().test(); + + GetVerticesErrorResponseMessage responseMessage = mock(GetVerticesErrorResponseMessage.class); + Vertex vertex = mock(Vertex.class); + when(vertex.getId()).thenReturn(id); + when(responseMessage.getVertexId()).thenReturn(id); + when(responseMessage.getHighestCommittedQC()).thenReturn(mock(QuorumCertificate.class)); + when(responseMessage.getHighestQC()).thenReturn(mock(QuorumCertificate.class)); + listener.get().handleMessage(mock(Peer.class), responseMessage); + + testObserver.awaitCount(1); + testObserver.assertValue(resp -> resp.getOpaque().equals(opaque)); + } + @Test public void when_send_response__then_message_central_will_send_response() { MessageCentralGetVerticesRequest request = mock(MessageCentralGetVerticesRequest.class); @@ -121,6 +156,17 @@ public void when_send_response__then_message_central_will_send_response() { verify(messageCentral, times(1)).send(eq(peer), any(GetVerticesResponseMessage.class)); } + @Test + public void when_send_error_response__then_message_central_will_send_error_response() { + MessageCentralGetVerticesRequest request = mock(MessageCentralGetVerticesRequest.class); + Peer peer = mock(Peer.class); + when(request.getVertexId()).thenReturn(mock(Hash.class)); + when(request.getRequestor()).thenReturn(peer); + QuorumCertificate qc = mock(QuorumCertificate.class); + network.sendGetVerticesErrorResponse(request, qc, qc); + verify(messageCentral, times(1)).send(eq(peer), any(GetVerticesErrorResponseMessage.class)); + } + @Test public void when_subscribed_to_rpc_requests__then_should_receive_requests() { Hash vertexId0 = mock(Hash.class); diff --git a/radixdlt/src/test/java/com/radixdlt/middleware2/network/TestBFTEventProcessorNetworkTest.java b/radixdlt/src/test/java/com/radixdlt/middleware2/network/TestBFTEventProcessorNetworkTest.java index 3747a7772..98f99a447 100644 --- a/radixdlt/src/test/java/com/radixdlt/middleware2/network/TestBFTEventProcessorNetworkTest.java +++ b/radixdlt/src/test/java/com/radixdlt/middleware2/network/TestBFTEventProcessorNetworkTest.java @@ -21,7 +21,7 @@ import com.radixdlt.consensus.ConsensusEvent; import com.radixdlt.consensus.Proposal; -import com.radixdlt.consensus.VertexStore.GetVerticesRequest; +import com.radixdlt.consensus.bft.VertexStore.GetVerticesRequest; import com.radixdlt.crypto.ECKeyPair; import com.radixdlt.crypto.ECPublicKey; import com.radixdlt.consensus.NewView;