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
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
* Copyright (C) 2024-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 Expand Up @@ -32,6 +32,7 @@
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;

/**
* Provides read-only methods for interacting with the underlying data storage mechanisms for
Expand All @@ -55,8 +56,8 @@ public ReadableNodeStoreImpl(@NonNull final ReadableStates states) {
}

@Override
public Roster snapshotOfFutureRoster() {
return constructFromNodesState(nodesState());
public Roster snapshotOfFutureRoster(Function<Long, Long> weightFunction) {
return constructFromNodesStateWithStakingInfoWeight(nodesState(), weightFunction);
}

/**
Expand Down Expand Up @@ -88,7 +89,9 @@ public Iterator<EntityNumber> keys() {
return nodesState().keys();
}

private Roster constructFromNodesState(@NonNull final ReadableKVState<EntityNumber, Node> nodesState) {
private Roster constructFromNodesStateWithStakingInfoWeight(
@NonNull final ReadableKVState<EntityNumber, Node> nodesState,
@NonNull final Function<Long, Long> weightProvider) {
final var rosterEntries = new ArrayList<RosterEntry>();
for (final var it = nodesState.keys(); it.hasNext(); ) {
final var nodeNumber = it.next();
Expand All @@ -102,7 +105,7 @@ private Roster constructFromNodesState(@NonNull final ReadableKVState<EntityNumb
if (!node.deleted()) {
final var entry = RosterEntry.newBuilder()
.nodeId(node.nodeId())
.weight(node.weight())
.weight(weightProvider.apply(node.nodeId()))
.gossipCaCertificate(node.gossipCaCertificate())
.gossipEndpoint(nodeEndpoints)
.build();
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 Expand Up @@ -126,7 +126,8 @@ void snapshotOfFutureRosterIncludesAllUndeletedDefinitions() {
given(readableStates.<EntityNumber, Node>get(anyString())).willReturn(nodesState);

subject = new ReadableNodeStoreImpl(readableStates);
final var result = subject.snapshotOfFutureRoster();
final var result = subject.snapshotOfFutureRoster(nodeId ->
nodesState.get(EntityNumber.newBuilder().number(nodeId).build()).weight());
org.assertj.core.api.Assertions.assertThat(result.rosterEntries())
.containsExactlyInAnyOrder(ROSTER_NODE_1, ROSTER_NODE_2, ROSTER_NODE_3);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
* Copyright (C) 2024-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 All @@ -22,6 +22,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Iterator;
import java.util.function.Function;

/**
* Provides read-only methods for interacting with the underlying data storage mechanisms for
Expand All @@ -35,9 +36,11 @@ public interface ReadableNodeStore {
* Constructs a new {@link Roster} object using the current info for each node defined in state.
* Accordingly, be warned that <b>this method iterates over all nodes.</b>
*
* @param weightFunction the function to use to determine the weight of each node
* from stakingNodeInfo
* @return a new roster, representing the most current node configurations available
*/
Roster snapshotOfFutureRoster();
Roster snapshotOfFutureRoster(Function<Long, Long> weightFunction);

/**
* Returns the node needed. If the node doesn't exist returns failureReason. If the
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 All @@ -44,6 +46,7 @@
import java.util.List;
import java.util.Spliterators;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import javax.inject.Inject;
import javax.inject.Singleton;
Expand Down Expand Up @@ -71,7 +74,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,17 +117,32 @@ public void handleTxBody(
final var nodeStore =
new ReadableNodeStoreImpl(state.getReadableStates(AddressBookService.NAME));
final var rosterStore = new WritableRosterStore(state.getWritableStates(RosterService.NAME));
final var candidateRoster = nodeStore.snapshotOfFutureRoster();
logger.info("Candidate roster is {}", candidateRoster);
final var stakingInfoStore =
new ReadableStakingInfoStoreImpl(state.getReadableStates(TokenService.NAME));

// update the candidate roster weights with weights from stakingNodeInfo map
final Function<Long, Long> weightFunction = nodeId -> {
final var stakingInfo = stakingInfoStore.get(nodeId);
if (stakingInfo != null && !stakingInfo.deleted()) {
return stakingInfo.stake();
}
// Default weight if no staking info is found or the node is deleted
return 0L;
};
final var candidateRoster = nodeStore.snapshotOfFutureRoster(weightFunction);
logger.info("Candidate roster with updated weights is {}", candidateRoster);
boolean rosterAccepted = false;
try {
rosterStore.putCandidateRoster(candidateRoster);
rosterAccepted = true;
} 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 if (networkAdminConfig.exportCandidateRoster()) {
// Having the option to export candidate-roster.json even before using the roster
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
Loading