Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use Stake value at the end of the day as weight in StakingNodeInfo #17285

Merged
merged 10 commits into from
Jan 10, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ message Node {
* of HBAR staked to that node.<br/>
* Consensus SHALL be calculated based on agreement of greater than `2/3`
* of the total `weight` value of all nodes on the network.
* <p>
* This field is deprecated and SHALL NOT be used when RosterLifecycle
* is enabled.
*/
uint64 weight = 8;
uint64 weight = 8 [deprecated = true];

/**
* A flag indicating this node is deleted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ message StakingNodeInfo {
* This is recomputed based on the `stake` of this node at midnight UTC of
* each day. If the `stake` of this node at that time is less than
* `min_stake`, then the weight SHALL be 0.<br/>
* The sum of all weights of nodes in the network SHALL be less than 500.
* <p>
* Given the following:
* <ul>
Expand All @@ -143,10 +142,11 @@ message StakingNodeInfo {
* <li>The `effective network stake` SHALL be calculated as ∑(`effective
* stake` of each node) for all nodes in the network address book.</li>
* </ul>
* The actual consensus weight for this node SHALL be calculated as
* __(500 * (`effective stake`/`effective network stake`))__
* <p>
* This field is deprecated and SHALL NOT be used when RosterLifecycle
* is enabled. The weight SHALL be same as the `effective_stake` described above.
*/
int32 weight = 10;
int32 weight = 10 [deprecated = true];

/**
* The total staking rewards in tinybars that MAY be collected by all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import com.hedera.node.app.service.addressbook.AddressBookService;
import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl;
import com.hedera.node.app.service.networkadmin.FreezeService;
import com.hedera.node.app.service.token.TokenService;
import com.hedera.node.app.service.token.impl.ReadableStakingInfoStoreImpl;
import com.hedera.node.config.data.NetworkAdminConfig;
import com.swirlds.config.api.Configuration;
import com.swirlds.platform.config.AddressBookConfig;
Expand Down Expand Up @@ -71,7 +73,7 @@ public PlatformStateUpdates(@NonNull final BiConsumer<Roster, Path> rosterExport
* Checks whether the given transaction body is a freeze transaction and eventually
* notifies the registered facility.
*
* @param state the current state
* @param state the current state
* @param txBody the transaction body
* @param config the configuration
*/
Expand Down Expand Up @@ -114,6 +116,8 @@ public void handleTxBody(
final var nodeStore =
new ReadableNodeStoreImpl(state.getReadableStates(AddressBookService.NAME));
final var rosterStore = new WritableRosterStore(state.getWritableStates(RosterService.NAME));
final var stakingInfoStore =
new ReadableStakingInfoStoreImpl(state.getReadableStates(TokenService.NAME));
final var candidateRoster = nodeStore.snapshotOfFutureRoster();
logger.info("Candidate roster is {}", candidateRoster);
boolean rosterAccepted = false;
Expand All @@ -123,8 +127,16 @@ public void handleTxBody(
} catch (Exception e) {
logger.warn("Candidate roster was rejected", e);
}
if (rosterAccepted && networkAdminConfig.exportCandidateRoster()) {
doExport(candidateRoster, networkAdminConfig);
if (rosterAccepted) {
// If the candidate roster needs to be exported, export the file
if (networkAdminConfig.exportCandidateRoster()) {
doExport(candidateRoster, networkAdminConfig);
} else {
// When the candidate roster is not exported, update the candidate
// roster weights with weights from stakingNodeInfo map
logger.info("Updating candidate roster weights");
updateCandidateRosterWeights(candidateRoster, stakingInfoStore, rosterStore);
}
}
} else if (networkAdminConfig.exportCandidateRoster()) {
// Having the option to export candidate-roster.json even before using the roster
Expand Down Expand Up @@ -153,6 +165,34 @@ public void handleTxBody(
}
}

/**
* Updates the candidate roster weights with the weights from the staking info store.
* If the staking info is not available for a node, the weight is set to 0. The updated
* candidate roster is then stored in the roster store.
*
* @param candidateRoster the candidate roster
* @param stakingInfoStore the staking info store
* @param rosterStore the roster store
*/
private void updateCandidateRosterWeights(
@NonNull final Roster candidateRoster,
@NonNull final ReadableStakingInfoStoreImpl stakingInfoStore,
@NonNull final WritableRosterStore rosterStore) {
final var newEntries = candidateRoster.rosterEntries().stream()
.map(entry -> {
final var nodeId = entry.nodeId();
final var stakingInfo = stakingInfoStore.get(nodeId);
long weight = 0;
if (stakingInfo != null && !stakingInfo.deleted()) {
weight = stakingInfo.stake();
}
return entry.copyBuilder().weight(weight).build();
})
.toList();
rosterStore.putCandidateRoster(
candidateRoster.copyBuilder().rosterEntries(newEntries).build());
}

private void doExport(@NonNull final Roster candidateRoster, @NonNull final NetworkAdminConfig networkAdminConfig) {
final var exportPath = Paths.get(networkAdminConfig.candidateRosterExportFile());
logger.info("Exporting candidate roster after PREPARE_UPGRADE to '{}'", exportPath.toAbsolutePath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.hedera.node.config.data.StakingConfig;
import com.hedera.node.config.types.StreamMode;
import com.swirlds.config.api.Configuration;
import com.swirlds.platform.config.AddressBookConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.time.Instant;
import java.time.LocalDate;
Expand Down Expand Up @@ -111,6 +112,8 @@ public boolean process(
stack.rollbackFullStack();
}
final var config = tokenContext.configuration();
final var useRosterLifecycle =
config.getConfigData(AddressBookConfig.class).useRosterLifecycle();
try {
final var nodeStore = newWritableNodeStore(stack, config);
final BiConsumer<Long, Integer> weightUpdates = (nodeId, weight) -> nodeStore.put(nodeStore
Expand All @@ -119,7 +122,7 @@ public boolean process(
.weight(weight)
.build());
final var streamBuilder = endOfStakingPeriodUpdater.updateNodes(
tokenContext, exchangeRateManager.exchangeRates(), weightUpdates);
tokenContext, exchangeRateManager.exchangeRates(), weightUpdates, useRosterLifecycle);
if (streamBuilder != null) {
stack.commitTransaction(streamBuilder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ void processUpdateCalledForGenesisTxn() {

subject.process(dispatch, stack, context, RECORDS, true, Instant.EPOCH);

verify(stakingPeriodCalculator).updateNodes(eq(context), eq(ExchangeRateSet.DEFAULT), any(BiConsumer.class));
verify(stakingPeriodCalculator)
.updateNodes(eq(context), eq(ExchangeRateSet.DEFAULT), any(BiConsumer.class), eq(false));
verify(exchangeRateManager).updateMidnightRates(stack);
}

Expand Down Expand Up @@ -167,7 +168,8 @@ void processUpdateCalledForNextPeriodWithRecordsStreamMode() {
.updateNodes(
argThat(stakingContext -> currentConsensusTime.equals(stakingContext.consensusTime())),
eq(ExchangeRateSet.DEFAULT),
any(BiConsumer.class));
any(BiConsumer.class),
eq(false));
verify(exchangeRateManager).updateMidnightRates(stack);
}

Expand All @@ -193,7 +195,8 @@ void processUpdateCalledForNextPeriodWithBlocksStreamMode() {
.updateNodes(
argThat(stakingContext -> currentConsensusTime.equals(stakingContext.consensusTime())),
eq(ExchangeRateSet.DEFAULT),
any(BiConsumer.class));
any(BiConsumer.class),
eq(false));
verify(exchangeRateManager).updateMidnightRates(stack);
}

Expand All @@ -203,7 +206,7 @@ void processUpdateExceptionIsCaught() {
given(exchangeRateManager.exchangeRates()).willReturn(ExchangeRateSet.DEFAULT);
doThrow(new RuntimeException("test exception"))
.when(stakingPeriodCalculator)
.updateNodes(any(), eq(ExchangeRateSet.DEFAULT), any(BiConsumer.class));
.updateNodes(any(), eq(ExchangeRateSet.DEFAULT), any(BiConsumer.class), eq(false));
given(blockStore.getLastBlockInfo())
.willReturn(BlockInfo.newBuilder()
.consTimeOfLastHandledTxn(new Timestamp(CONSENSUS_TIME_1234567.getEpochSecond(), 0))
Expand All @@ -215,7 +218,8 @@ void processUpdateExceptionIsCaught() {

Assertions.assertThatNoException()
.isThrownBy(() -> subject.process(dispatch, stack, context, RECORDS, false, Instant.EPOCH));
verify(stakingPeriodCalculator).updateNodes(eq(context), eq(ExchangeRateSet.DEFAULT), any(BiConsumer.class));
verify(stakingPeriodCalculator)
.updateNodes(eq(context), eq(ExchangeRateSet.DEFAULT), any(BiConsumer.class), eq(false));
verify(exchangeRateManager).updateMidnightRates(stack);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_STATES_KEY;
import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY;
import static com.hedera.node.app.service.networkadmin.impl.schemas.V0490FreezeSchema.FREEZE_TIME_KEY;
import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.STAKING_INFO_KEY;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -44,13 +45,15 @@
import com.hedera.hapi.node.state.roster.Roster;
import com.hedera.hapi.node.state.roster.RosterState;
import com.hedera.hapi.node.state.roster.RoundRosterPair;
import com.hedera.hapi.node.state.token.StakingNodeInfo;
import com.hedera.hapi.node.token.CryptoTransferTransactionBody;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.hapi.platform.state.PlatformState;
import com.hedera.node.app.fixtures.state.FakeState;
import com.hedera.node.app.roster.RosterService;
import com.hedera.node.app.service.addressbook.AddressBookService;
import com.hedera.node.app.service.networkadmin.FreezeService;
import com.hedera.node.app.service.token.TokenService;
import com.hedera.node.app.spi.fixtures.TransactionFactory;
import com.hedera.node.config.testfixtures.HederaTestConfigBuilder;
import com.hedera.pbj.runtime.io.buffer.Bytes;
Expand Down Expand Up @@ -83,9 +86,10 @@ public class PlatformStateUpdatesTest implements TransactionFactory {
private PlatformStateUpdates subject;
private AtomicReference<Timestamp> freezeTimeBackingStore;
private AtomicReference<PlatformState> platformStateBackingStore;
private AtomicReference<RosterState> rosterStateBackingStore = new AtomicReference<>(ROSTER_STATE);
private AtomicReference<RosterState> rosterStateBackingStore;
private ConcurrentHashMap<ProtoBytes, Roster> rosters = new ConcurrentHashMap<>();
private ConcurrentHashMap<EntityNumber, Node> nodes = new ConcurrentHashMap<>();
private ConcurrentHashMap<EntityNumber, StakingNodeInfo> stakingInfo = new ConcurrentHashMap<>();

@Mock(strictness = LENIENT)
protected WritableStates writableStates;
Expand All @@ -97,6 +101,7 @@ public class PlatformStateUpdatesTest implements TransactionFactory {
void setUp() {
freezeTimeBackingStore = new AtomicReference<>(null);
platformStateBackingStore = new AtomicReference<>(V0540PlatformStateSchema.UNINITIALIZED_PLATFORM_STATE);
rosterStateBackingStore = new AtomicReference<>(ROSTER_STATE);
when(writableStates.getSingleton(FREEZE_TIME_KEY))
.then(invocation -> new WritableSingletonStateBase<>(
FREEZE_TIME_KEY, freezeTimeBackingStore::get, freezeTimeBackingStore::set));
Expand All @@ -111,7 +116,8 @@ void setUp() {
.addService(AddressBookService.NAME, Map.of(NODES_KEY, nodes))
.addService(
PlatformStateService.NAME,
Map.of(V0540PlatformStateSchema.PLATFORM_STATE_KEY, platformStateBackingStore));
Map.of(V0540PlatformStateSchema.PLATFORM_STATE_KEY, platformStateBackingStore))
.addService(TokenService.NAME, Map.of(STAKING_INFO_KEY, stakingInfo));

subject = new PlatformStateUpdates(rosterExportHelper);
}
Expand Down Expand Up @@ -188,7 +194,7 @@ void testFreezeUpgradeWhenKeying() {
.freeze(FreezeTransactionBody.newBuilder().freezeType(FREEZE_UPGRADE));

// when
subject.handleTxBody(state, txBody.build(), configWith(true, false));
subject.handleTxBody(state, txBody.build(), configWith(true, false, true));

// then
final var platformState = platformStateBackingStore.get();
Expand All @@ -205,7 +211,7 @@ void testFreezeUpgradeWhenKeyingButNotUsingRosterLifecycle() {
.freeze(FreezeTransactionBody.newBuilder().freezeType(FREEZE_UPGRADE));

// when
subject.handleTxBody(state, txBody.build(), configWith(false, false));
subject.handleTxBody(state, txBody.build(), configWith(false, false, true));

// then
final var platformState = platformStateBackingStore.get();
Expand All @@ -229,7 +235,7 @@ void putsCandidateRosterWhenNotKeyingButUsingRosterLifecycle() {
.build());

// when
subject.handleTxBody(state, txBody.build(), configWith(true, true));
subject.handleTxBody(state, txBody.build(), configWith(true, true, true));

// then
final var captor = ArgumentCaptor.forClass(Path.class);
Expand All @@ -252,7 +258,7 @@ void worksAroundFailureToPutCandidateRoster() {
.gossipEndpoint(new ServiceEndpoint(Bytes.EMPTY, 50211, "test.org"))
.build());

subject.handleTxBody(state, txBody.build(), configWith(true, true));
subject.handleTxBody(state, txBody.build(), configWith(true, true, true));

verify(rosterExportHelper, never()).accept(any(), any());
}
Expand All @@ -273,7 +279,7 @@ void exportsCandidateRosterIfRequestedEvenWhenNotUsingRosterLifecycle() {
.build());

// when
subject.handleTxBody(state, txBody.build(), configWith(false, true));
subject.handleTxBody(state, txBody.build(), configWith(false, true, true));

// then
final var captor = ArgumentCaptor.forClass(Path.class);
Expand All @@ -282,11 +288,68 @@ void exportsCandidateRosterIfRequestedEvenWhenNotUsingRosterLifecycle() {
assertEquals("candidate-network.json", path.getFileName().toString());
}

private Configuration configWith(final boolean useRosterLifecycle, final boolean createCandidateRoster) {
@Test
void updatesCandidateRosterWeightsWhenNotExportingAndRosterLifecycleEnabled() {
final var freezeTime = Timestamp.newBuilder().seconds(123L).nanos(456).build();
freezeTimeBackingStore.set(freezeTime);
final var txBody = TransactionBody.newBuilder()
.freeze(FreezeTransactionBody.newBuilder().freezeType(PREPARE_UPGRADE));
nodes.put(
new EntityNumber(0L),
Node.newBuilder()
.nodeId(0L)
.weight(1)
.gossipCaCertificate(Bytes.fromHex("0123"))
.gossipEndpoint(new ServiceEndpoint(Bytes.EMPTY, 50211, "test.org"))
.build());
nodes.put(
new EntityNumber(1L),
Node.newBuilder()
.nodeId(1L)
.weight(2L)
.gossipCaCertificate(Bytes.fromHex("0123"))
.gossipEndpoint(new ServiceEndpoint(Bytes.EMPTY, 50211, "test.org"))
.build());
nodes.put(
new EntityNumber(2L),
Node.newBuilder()
.nodeId(2L)
.weight(3L)
.gossipCaCertificate(Bytes.fromHex("0123"))
.gossipEndpoint(new ServiceEndpoint(Bytes.EMPTY, 50211, "test.org"))
.build());
stakingInfo.put(
new EntityNumber(0L),
StakingNodeInfo.newBuilder().stake(1000).weight(1).build());
stakingInfo.put(
new EntityNumber(1),
StakingNodeInfo.newBuilder().stake(1000).deleted(true).weight(1).build());

subject.handleTxBody(state, txBody.build(), configWith(true, true, false));
final var candidateRosterHash = state.getWritableStates(RosterService.NAME)
.<RosterState>getSingleton("ROSTER_STATE")
.get()
.candidateRosterHash();
final var candidateRoster = state.getWritableStates(RosterService.NAME)
.<ProtoBytes, Roster>get("ROSTERS")
.get(new ProtoBytes(candidateRosterHash));
assertEquals(candidateRoster.rosterEntries().size(), 3);
// Updates the stake value for node 0 as weight in the candidate roster
assertEquals(candidateRoster.rosterEntries().get(0).weight(), 1000);
// node 1 is deleted, so weight is zero
assertEquals(candidateRoster.rosterEntries().get(1).weight(), 0);
// node 2 is newly added in the candidate roster, so weight will be zero
assertEquals(candidateRoster.rosterEntries().get(2).weight(), 0);
}

private Configuration configWith(
final boolean useRosterLifecycle,
final boolean createCandidateRoster,
final boolean exportCandidateRoster) {
return HederaTestConfigBuilder.create()
.withValue("addressBook.createCandidateRosterOnPrepareUpgrade", "" + createCandidateRoster)
.withValue("addressBook.useRosterLifecycle", "" + useRosterLifecycle)
.withValue("networkAdmin.exportCandidateRoster", "true")
.withValue("networkAdmin.exportCandidateRoster", "" + exportCandidateRoster)
.withValue("networkAdmin.candidateRosterExportFile", "candidate-network.json")
.getOrCreateConfig();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
* Copyright (C) 2023-2025 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Loading
Loading