Skip to content

Commit

Permalink
feat: utility to retrieve a Roster for a round (#15259)
Browse files Browse the repository at this point in the history
Signed-off-by: Anthony Petrov <[email protected]>
  • Loading branch information
anthony-swirldslabs authored Aug 29, 2024
1 parent 253c7f3 commit 4d517d6
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.swirlds.platform.roster;

/**
* An exception thrown by the RosterValidator when a given Roster is invalid.
* An exception thrown by the RosterRetriever.buildRoster() when a given AddressBook is invalid.
*/
public class InvalidAddressBookException extends RuntimeException {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,31 @@ private RosterRetriever() {}
*/
@NonNull
public static Roster retrieve(@NonNull final State state) {
final Bytes activeRosterHash = getActiveRosterHash(state);
final Roster roster = retrieve(state, getRound(state));
if (roster != null) {
return roster;
}

// Either rosters haven't been populated in the RosterState/Map yet,
// or the current round predates the introduction of rosters in the state.
// So use an AddressBook from the PlatformState to build a roster:
final ReadablePlatformStateStore readablePlatformStateStore =
new ReadablePlatformStateStore(state.getReadableStates(PlatformStateService.NAME));
return buildRoster(readablePlatformStateStore.getAddressBook());
}

/**
* Retrieve an active Roster from the state for a given round.
* <p>
* This method first checks the RosterState/RosterMap entities,
* and if they contain a roster for the given round, then returns it.
* If there's not a roster defined for a given round, then null is returned.
*
* @return an active Roster for the given round of the state, or null
*/
@Nullable
public static Roster retrieve(@NonNull final State state, final long round) {
final Bytes activeRosterHash = getActiveRosterHash(state, round);
if (activeRosterHash != null) {
final ReadableKVState<ProtoBytes, Roster> rosterMap =
state.getReadableStates(RosterStateId.NAME).get(RosterStateId.ROSTER_KEY);
Expand All @@ -73,23 +97,21 @@ public static Roster retrieve(@NonNull final State state) {
}
}

final ReadablePlatformStateStore readablePlatformStateStore =
new ReadablePlatformStateStore(state.getReadableStates(PlatformStateService.NAME));
return buildRoster(readablePlatformStateStore.getAddressBook());
return null;
}

/**
* Retrieve a hash of the active roster for the current round of the state,
* Retrieve a hash of the active roster for a given round of the state,
* or null if the roster is unknown for that round.
* A roster may be unknown if the RosterState hasn't been populated yet,
* or the current round of the state predates the implementation of the Roster.
* or the given round of the state predates the implementation of the Roster.
*
* @param state a state
* @param round a round number
* @return a Bytes object with the roster hash, or null
*/
@Nullable
public static Bytes getActiveRosterHash(@NonNull final State state) {
final long round = getRound(state);
public static Bytes getActiveRosterHash(@NonNull final State state, final long round) {
final ReadableSingletonState<RosterState> rosterState =
state.getReadableStates(RosterStateId.NAME).getSingleton(RosterStateId.ROSTER_STATES_KEY);
// replace with binary search when/if the list size becomes unreasonably large (100s of entries or more)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ public class RosterRetrieverTests {
.build()))
.build();

private static final Roster ROSTER_FROM_ADDRESS_BOOK = Roster.newBuilder()
.rosters(List.of(
RosterEntry.newBuilder()
.nodeId(1L)
.weight(1L)
.gossipCaCertificate(getCertBytes(CERTIFICATE_1))
.gossipEndpoint(List.of(
ServiceEndpoint.newBuilder()
.domainName("external1.com")
.port(111)
.build(),
ServiceEndpoint.newBuilder()
.ipAddressV4(Bytes.wrap(new byte[] {(byte) 192, (byte) 168, 0, 1}))
.port(222)
.build()))
.build(),
RosterEntry.newBuilder()
.nodeId(2L)
.weight(111L)
.gossipCaCertificate(getCertBytes(CERTIFICATE_2))
.gossipEndpoint(List.of(ServiceEndpoint.newBuilder()
.ipAddressV4(Bytes.wrap(new byte[] {10, 0, 55, 66}))
.port(222)
.build()))
.build(),
RosterEntry.newBuilder()
.nodeId(3L)
.weight(3L)
.gossipCaCertificate(getCertBytes(CERTIFICATE_3))
.gossipEndpoint(List.of(ServiceEndpoint.newBuilder()
.domainName("external3.com")
.port(111)
.build()))
.build()))
.build();

@Mock
private State state;

Expand Down Expand Up @@ -197,9 +233,8 @@ private static Stream<Arguments> provideArgumentsForGetActiveRosterHash() {

@ParameterizedTest
@MethodSource("provideArgumentsForGetActiveRosterHash")
void testGetActiveRosterHash(final long round, final Bytes activeRosterHash) {
doReturn(round).when(consensusSnapshot).round();
assertEquals(activeRosterHash, RosterRetriever.getActiveRosterHash(state));
void testGetActiveRosterHashForRound(final long round, final Bytes activeRosterHash) {
assertEquals(activeRosterHash, RosterRetriever.getActiveRosterHash(state, round));
}

@Test
Expand All @@ -209,7 +244,7 @@ void testRetrieve() {

private static Stream<Arguments> provideArgumentsForRetrieveParametrized() {
return Stream.of(
// Arguments.of(554L, null),
Arguments.of(554L, ROSTER_FROM_ADDRESS_BOOK),
Arguments.of(555L, ROSTER_555),
Arguments.of(556L, ROSTER_555),
Arguments.of(665L, ROSTER_555),
Expand All @@ -227,54 +262,37 @@ void testRetrieveParametrized(final long round, final Roster roster) {
assertEquals(roster, RosterRetriever.retrieve(state));
}

private static Stream<Arguments> provideArgumentsForRetrieveForRound() {
return Stream.of(
Arguments.of(554L, null),
Arguments.of(555L, ROSTER_555),
Arguments.of(556L, ROSTER_555),
Arguments.of(665L, ROSTER_555),
Arguments.of(666L, ROSTER_666),
Arguments.of(667L, ROSTER_666),
Arguments.of(776L, ROSTER_666),
Arguments.of(777L, ROSTER_777),
Arguments.of(778L, ROSTER_777));
}

@ParameterizedTest
@MethodSource("provideArgumentsForRetrieveForRound")
void testRetrieveForRound(final long round, final Roster roster) {
assertEquals(roster, RosterRetriever.retrieve(state, round));
}

@Test
void testRetrieveAddressBook() {
Roster expected = Roster.newBuilder()
.rosters(List.of(
RosterEntry.newBuilder()
.nodeId(1L)
.weight(1L)
.gossipCaCertificate(getCertBytes(CERTIFICATE_1))
.gossipEndpoint(List.of(
ServiceEndpoint.newBuilder()
.domainName("external1.com")
.port(111)
.build(),
ServiceEndpoint.newBuilder()
.ipAddressV4(Bytes.wrap(new byte[] {(byte) 192, (byte) 168, 0, 1}))
.port(222)
.build()))
.build(),
RosterEntry.newBuilder()
.nodeId(2L)
.weight(111L)
.gossipCaCertificate(getCertBytes(CERTIFICATE_2))
.gossipEndpoint(List.of(ServiceEndpoint.newBuilder()
.ipAddressV4(Bytes.wrap(new byte[] {10, 0, 55, 66}))
.port(222)
.build()))
.build(),
RosterEntry.newBuilder()
.nodeId(3L)
.weight(3L)
.gossipCaCertificate(getCertBytes(CERTIFICATE_3))
.gossipEndpoint(List.of(ServiceEndpoint.newBuilder()
.domainName("external3.com")
.port(111)
.build()))
.build()))
.build();

// First try a very old round for which there's not a roster
doReturn(554L).when(consensusSnapshot).round();
assertEquals(expected, RosterRetriever.retrieve(state));
assertEquals(ROSTER_FROM_ADDRESS_BOOK, RosterRetriever.retrieve(state));

// Then try a newer round, but remove the roster from the RosterMap
doReturn(666L).when(consensusSnapshot).round();
doReturn(null)
.when(rosterMap)
.get(eq(ProtoBytes.newBuilder().value(HASH_666).build()));
assertEquals(expected, RosterRetriever.retrieve(state));
assertEquals(ROSTER_FROM_ADDRESS_BOOK, RosterRetriever.retrieve(state));
}

private static X509Certificate randomX509Certificate() {
Expand Down

0 comments on commit 4d517d6

Please sign in to comment.