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: Hook midnight rate updates into handle workflow #10322

Merged
merged 4 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,11 @@ private void handleUserTransaction(
try {
// If this is the first user transaction after midnight, then handle staking updates prior to handling the
// transaction itself.
stakingPeriodTimeHook.process(tokenServiceContext);
stakingPeriodTimeHook.process(stack, tokenServiceContext);
} catch (final Exception e) {
// If anything goes wrong, we log the error and continue
logger.error("Failed to process staking period time hook", e);
}
// @future('7836'): update the exchange rate and call from here

// Consensus hooks have now had a chance to publish any records from migrations; therefore we can begin handling
// the user transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
import static java.util.Objects.requireNonNull;

import com.google.common.annotations.VisibleForTesting;
import com.hedera.node.app.fees.ExchangeRateManager;
import com.hedera.node.app.records.ReadableBlockRecordStore;
import com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUpdater;
import com.hedera.node.app.service.token.records.TokenContext;
import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl;
import com.hedera.node.config.data.StakingConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.time.Instant;
Expand All @@ -36,17 +38,21 @@
import org.apache.logging.log4j.Logger;

/**
* A {@link ConsensusTimeHook} implementation that handles the daily staking period updates
* Handles the daily staking period updates
*/
@Singleton
public class StakingPeriodTimeHook implements ConsensusTimeHook {
public class StakingPeriodTimeHook {
private static final Logger logger = LogManager.getLogger(StakingPeriodTimeHook.class);

private final EndOfStakingPeriodUpdater stakingCalculator;
private final ExchangeRateManager exchangeRateManager;

@Inject
public StakingPeriodTimeHook(@NonNull final EndOfStakingPeriodUpdater stakingPeriodCalculator) {
public StakingPeriodTimeHook(
@NonNull final EndOfStakingPeriodUpdater stakingPeriodCalculator,
@NonNull final ExchangeRateManager exchangeRateManager) {
this.stakingCalculator = requireNonNull(stakingPeriodCalculator);
this.exchangeRateManager = requireNonNull(exchangeRateManager);
}

/**
Expand All @@ -58,25 +64,38 @@
* <b>which should only happen on node startup.</b> The node should therefore run this process
* to catch up on updates and distributions when first coming online.
*/
@Override
public void process(@NonNull final TokenContext context) {
requireNonNull(context, "context must not be null");
final var blockStore = context.readableStore(ReadableBlockRecordStore.class);
void process(@NonNull final SavepointStackImpl stack, @NonNull final TokenContext tokenContext) {
requireNonNull(stack, "stack must not be null");
requireNonNull(tokenContext, "tokenContext must not be null");
final var blockStore = tokenContext.readableStore(ReadableBlockRecordStore.class);
final var consensusTimeOfLastHandledTxn = blockStore.getLastBlockInfo().consTimeOfLastHandledTxn();

final var consensusTime = context.consensusTime();
final var consensusTime = tokenContext.consensusTime();
if (consensusTimeOfLastHandledTxn == null
|| (consensusTime.getEpochSecond() > consensusTimeOfLastHandledTxn.seconds()
&& isNextStakingPeriod(
consensusTime,
Instant.ofEpochSecond(
consensusTimeOfLastHandledTxn.seconds(), consensusTimeOfLastHandledTxn.nanos()),
context))) {
// Handle the daily staking distributions and updates
tokenContext))) {
try {
stakingCalculator.updateNodes(context);
// handle staking updates
stakingCalculator.updateNodes(tokenContext);
stack.commitFullStack();
} catch (final Exception e) {
// If anything goes wrong, we log the error and continue
logger.error("CATASTROPHIC failure updating end-of-day stakes", e);
stack.rollbackFullStack();
}

try {
// Update the exchange rate
exchangeRateManager.updateMidnightRates(stack);
stack.commitFullStack();
} catch (final Exception e) {

Check warning on line 95 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/StakingPeriodTimeHook.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/StakingPeriodTimeHook.java#L95

Added line #L95 was not covered by tests
// If anything goes wrong, we log the error and continue
logger.error("CATASTROPHIC failure updating midnight rates", e);
stack.rollbackFullStack();

Check warning on line 98 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/StakingPeriodTimeHook.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/StakingPeriodTimeHook.java#L97-L98

Added lines #L97 - L98 were not covered by tests
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import com.hedera.node.app.service.token.records.GenesisAccountRecordBuilder;
import com.hedera.node.app.service.token.records.TokenContext;
import com.hedera.node.app.spi.workflows.record.GenesisRecordsBuilder;
import com.hedera.node.app.workflows.handle.ConsensusTimeHook;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Collections;
Expand All @@ -45,7 +44,7 @@
* the corresponding synthetic records when a consensus time becomes available.
*/
@Singleton
public class GenesisRecordsConsensusHook implements GenesisRecordsBuilder, ConsensusTimeHook {
public class GenesisRecordsConsensusHook implements GenesisRecordsBuilder {
private static final Logger log = LogManager.getLogger(GenesisRecordsConsensusHook.class);
private static final String SYSTEM_ACCOUNT_CREATION_MEMO = "Synthetic system creation";
private static final String STAKING_MEMO = "Release 0.24.1 migration record";
Expand All @@ -66,7 +65,6 @@ public class GenesisRecordsConsensusHook implements GenesisRecordsBuilder, Conse
* <p>
* It would be great if we could find a way to not have to invoke this method multiple times...
*/
@Override
public void process(@NonNull final TokenContext context) {
final var blockStore = context.readableStore(ReadableBlockRecordStore.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1698,7 +1698,7 @@ void testSimpleRun() {
void testConsensusTimeHooksCalled() {
workflow.handleRound(state, dualState, round);
verify(genesisRecordsTimeHook).process(notNull());
verify(stakingPeriodTimeHook).process(notNull());
verify(stakingPeriodTimeHook).process(notNull(), notNull());
}

private SingleTransactionRecord getRecordFromStream() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@
import static com.hedera.node.app.service.token.impl.handlers.staking.StakePeriodManager.DEFAULT_STAKING_PERIOD_MINS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

import com.hedera.hapi.node.base.Timestamp;
import com.hedera.hapi.node.state.blockrecords.BlockInfo;
import com.hedera.node.app.fees.ExchangeRateManager;
import com.hedera.node.app.records.ReadableBlockRecordStore;
import com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUpdater;
import com.hedera.node.app.service.token.records.TokenContext;
import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl;
import com.hedera.node.config.data.StakingConfig;
import com.hedera.node.config.testfixtures.HederaTestConfigBuilder;
import com.swirlds.config.api.Configuration;
Expand All @@ -49,30 +50,40 @@ class StakingPeriodTimeHookTest {
@Mock
private EndOfStakingPeriodUpdater stakingPeriodCalculator;

@Mock
private ExchangeRateManager exchangeRateManager;

@Mock(strictness = Mock.Strictness.LENIENT)
private TokenContext context;

@Mock
private ReadableBlockRecordStore blockStore;

@Mock
private SavepointStackImpl stack;

private StakingPeriodTimeHook subject;

@BeforeEach
void setUp() {
given(context.readableStore(ReadableBlockRecordStore.class)).willReturn(blockStore);

subject = new StakingPeriodTimeHook(stakingPeriodCalculator);
subject = new StakingPeriodTimeHook(stakingPeriodCalculator, exchangeRateManager);
}

@SuppressWarnings("DataFlowIssue")
@Test
void nullArgConstructor() {
Assertions.assertThatThrownBy(() -> new StakingPeriodTimeHook(null)).isInstanceOf(NullPointerException.class);
Assertions.assertThatThrownBy(() -> new StakingPeriodTimeHook(null, exchangeRateManager))
.isInstanceOf(NullPointerException.class);
Assertions.assertThatThrownBy(() -> new StakingPeriodTimeHook(stakingPeriodCalculator, null))
.isInstanceOf(NullPointerException.class);
}

@Test
void processUpdateSkippedForPreviousPeriod() {
verifyNoInteractions(stakingPeriodCalculator);
verifyNoInteractions(exchangeRateManager);
}

@Test
Expand All @@ -83,9 +94,10 @@ void processUpdateCalledForNullConsensusTime() {
.build());
given(context.consensusTime()).willReturn(CONSENSUS_TIME_1234567);

subject.process(context);
subject.process(stack, context);

verify(stakingPeriodCalculator).updateNodes(notNull());
verify(stakingPeriodCalculator).updateNodes(context);
verify(exchangeRateManager).updateMidnightRates(stack);
}

@Test
Expand All @@ -99,9 +111,10 @@ void processUpdateSkippedForPreviousConsensusTime() {
.nanos(CONSENSUS_TIME_1234567.getNano()))
.build());

subject.process(context);
subject.process(stack, context);

verifyNoInteractions(stakingPeriodCalculator);
verifyNoInteractions(exchangeRateManager);
}

@Test
Expand All @@ -122,10 +135,11 @@ void processUpdateCalledForNextPeriod() {
currentConsensusTime, CONSENSUS_TIME_1234567, context))
.isTrue();

subject.process(context);
subject.process(stack, context);

verify(stakingPeriodCalculator)
.updateNodes(argThat(stakingContext -> currentConsensusTime.equals(stakingContext.consensusTime())));
verify(exchangeRateManager).updateMidnightRates(stack);
}

@Test
Expand All @@ -139,8 +153,9 @@ void processUpdateExceptionIsCaught() {
.consTimeOfLastHandledTxn((Timestamp) null)
.build());

Assertions.assertThatNoException().isThrownBy(() -> subject.process(context));
verify(stakingPeriodCalculator).updateNodes(any());
Assertions.assertThatNoException().isThrownBy(() -> subject.process(stack, context));
verify(stakingPeriodCalculator).updateNodes(context);
verify(exchangeRateManager).updateMidnightRates(stack);
}

@Test
Expand Down
Loading