55
66import com .hedera .hapi .node .base .SemanticVersion ;
77import com .hedera .hapi .platform .event .StateSignatureTransaction ;
8+ import com .swirlds .common .config .StateCommonConfig ;
9+ import com .swirlds .config .api .Configuration ;
10+ import com .swirlds .platform .system .InitTrigger ;
811import com .swirlds .state .lifecycle .Schema ;
912import com .swirlds .state .spi .WritableStates ;
1013import edu .umd .cs .findbugs .annotations .NonNull ;
14+ import java .io .IOException ;
15+ import java .io .UncheckedIOException ;
16+ import java .nio .file .Files ;
17+ import java .nio .file .Path ;
1118import java .util .Set ;
1219import java .util .concurrent .ConcurrentHashMap ;
1320import java .util .function .Consumer ;
1421import org .apache .logging .log4j .LogManager ;
1522import org .apache .logging .log4j .Logger ;
1623import org .hiero .consensus .model .event .Event ;
24+ import org .hiero .consensus .model .hashgraph .ConsensusConstants ;
1725import org .hiero .consensus .model .hashgraph .Round ;
26+ import org .hiero .consensus .model .node .NodeId ;
1827import org .hiero .consensus .model .transaction .ScopedSystemTransaction ;
28+ import org .hiero .otter .fixtures .app .OtterAppState ;
1929import org .hiero .otter .fixtures .app .OtterService ;
2030import org .hiero .otter .fixtures .app .OtterTransaction ;
31+ import org .jetbrains .annotations .NotNull ;
2132
2233/**
2334 * A service that ensures the consistency of rounds and transactions sent by the platform to the execution layer for
2435 * handling. It checks these aspects of consistency:
2536 * <ol>
2637 * <li>Consensus rounds increase in number monotonically</li>
2738 * <li>Consensus rounds are received only once</li>
28- * <li>Differences in rounds or transactions sent to {@link #recordRound(Round)} on different nodes will cause an ISS</li>
29- * <li>Consensus transactions were previous received in preHandle</li>
30- * <li>After a restart, any rounds that reach consensus in PCES replay exactly match the rounds calculated previously.</li>
39+ * <li>Differences in rounds or transactions recorded in the {@link ConsistencyServiceRoundHistory} on different nodes will cause an ISS</li>
40+ * <li>Transactions are pre-handled only once</li>
41+ * <li>Consensus transactions were previously received in pre-handle</li>
42+ * <li>After a restart, any rounds that reach consensus during PCES replay exactly match the rounds calculated previously.</li>
3143 * </ol>
3244 */
3345public class ConsistencyService implements OtterService {
@@ -40,23 +52,86 @@ public class ConsistencyService implements OtterService {
4052 /** A set of transaction nonce values seen in pre-handle that have not yet been handled. */
4153 private final Set <Long > transactionsAwaitingHandle = ConcurrentHashMap .newKeySet ();
4254
55+ /** A history of all rounds and transaction nonce values contained within. */
56+ private final ConsistencyServiceRoundHistory roundHistory = new ConsistencyServiceRoundHistory ();
57+
58+ /** The round number of the previous round handled. */
59+ private long previousRoundHandled = ConsensusConstants .ROUND_UNDEFINED ;
60+
61+ /**
62+ * {@inheritDoc}
63+ */
64+ public void initialize (
65+ @ NonNull final InitTrigger trigger ,
66+ @ NonNull final NodeId selfId ,
67+ @ NonNull final Configuration configuration ,
68+ @ NonNull final OtterAppState state ) {
69+ if (trigger != InitTrigger .GENESIS && trigger != InitTrigger .RESTART ) {
70+ return ;
71+ }
72+ final StateCommonConfig stateConfig = configuration .getConfigData (StateCommonConfig .class );
73+ final ConsistencyServiceConfig consistencyServiceConfig =
74+ configuration .getConfigData (ConsistencyServiceConfig .class );
75+
76+ final Path historyFileDirectory = stateConfig
77+ .savedStateDirectory ()
78+ .resolve (consistencyServiceConfig .historyFileDirectory ())
79+ .resolve (Long .toString (selfId .id ()));
80+ try {
81+ Files .createDirectories (historyFileDirectory );
82+ } catch (final IOException e ) {
83+ log .error (EXCEPTION .getMarker (), "Unable to create log file directory" , e );
84+ throw new UncheckedIOException ("unable to set up file system for consistency data" , e );
85+ }
86+
87+ final Path historyFilePath = historyFileDirectory .resolve (consistencyServiceConfig .historyFileName ());
88+ roundHistory .init (historyFilePath );
89+ }
90+
91+ /**
92+ * {@inheritDoc}
93+ */
94+ @ Override
95+ public void destroy () {
96+ roundHistory .close ();
97+ }
98+
4399 /**
44- * Records the contents of all rounds, even empty ones. This method calculates a running hash that includes the
100+ * Records the contents of all rounds, even empty ones. This method calculates a running checksum that includes the
45101 * round number and all transactions and stores the number of rounds handled in the state.
46102 *
47103 * @param writableStates the writable states used to modify the consistency state
48104 * @param round the round to handle
49105 */
50106 @ Override
51- public void handleRound (@ NonNull final WritableStates writableStates , @ NonNull final Round round ) {
52- new WritableConsistencyStateStore (writableStates )
107+ public void onRoundStart (@ NonNull final WritableStates writableStates , @ NonNull final Round round ) {
108+ verifyRoundIncreases (round );
109+
110+ final WritableConsistencyStateStore store = new WritableConsistencyStateStore (writableStates )
53111 .accumulateRunningChecksum (round .getRoundNum ())
54112 .incrementRoundsHandled ();
113+ roundHistory .onRoundStart (round , store .getRunningChecksum ());
114+ }
115+
116+ private void verifyRoundIncreases (@ NonNull final Round round ) {
117+ if (previousRoundHandled == ConsensusConstants .ROUND_UNDEFINED ) {
118+ previousRoundHandled = round .getRoundNum ();
119+ return ;
120+ }
121+
122+ final long newRoundNumber = round .getRoundNum ();
123+
124+ // make sure round numbers always increase
125+ if (newRoundNumber <= previousRoundHandled ) {
126+ final String error = "Round " + newRoundNumber + " is not greater than round " + previousRoundHandled ;
127+ log .error (EXCEPTION .getMarker (), error );
128+ }
129+
130+ previousRoundHandled = round .getRoundNum ();
55131 }
56132
57133 /**
58- * This method updates the running hash that includes the contents of all
59- * transactions.
134+ * This method updates the running hash that includes the contents of all transactions.
60135 */
61136 @ Override
62137 public void handleTransaction (
@@ -67,13 +142,14 @@ public void handleTransaction(
67142 final long transactionNonce = transaction .getNonce ();
68143 new WritableConsistencyStateStore (writableStates ).accumulateRunningChecksum (transactionNonce );
69144 if (!transactionsAwaitingHandle .remove (transactionNonce )) {
70- log .error (EXCEPTION .getMarker (), "Transaction {} was not prehandled ." , transactionNonce );
145+ log .error (EXCEPTION .getMarker (), "Transaction {} was not pre-handled ." , transactionNonce );
71146 }
147+ roundHistory .onTransaction (transaction );
72148 }
73149
74150 /**
75- * This method records the checksum of all transactions that are pre-handled, so that we can verify
76- * that all consensus transactions were previously pre-handled.
151+ * This method records the checksum of all transactions that are pre-handled, so that we can verify that all
152+ * consensus transactions were previously pre-handled.
77153 *
78154 * @param event the event that contains the transaction
79155 * @param transaction the transaction being pre-handled
@@ -90,23 +166,12 @@ public void preHandleTransaction(
90166 }
91167 }
92168
93- private void recordRound (@ NonNull final Round round ) {
94- // FUTURE WORK: Write the round data to in-memory structure and disk. Write to in-memory structure
95- // so we can verify that rounds increase monotonically (no rounds are repeated or skipped). Write to
96- // disk so that we can verify that the same rounds reach consensus after a restart during PCES replay.
97-
98- // FUTURE WORK: Compare the round to rounds previous recorded in memory and do basic validations, like
99- // checking that the round number is one greater than the previous round number, and that all transactions
100- // were previously received in prehandle.
101- }
102-
103- public void initialize () {
104- // FUTURE WORK: Read round data from disk (written in recordRound()) into in-memory structure.
105- }
106-
107- public void recordPreHandleTransactions (@ NonNull final Event event ) {
108- // FUTURE WORK: Record the prehandle transactions so that we can verify all
109- // consensus transactions were previously sent to prehandle.
169+ /**
170+ * {@inheritDoc}
171+ */
172+ @ Override
173+ public void onRoundComplete (@ NotNull final Round round ) {
174+ roundHistory .onRoundComplete ();
110175 }
111176
112177 /**
0 commit comments