@@ -73,6 +73,17 @@ public class ConsensusUpdateTopicHandler implements TransactionHandler {
7373
7474 private final ConsensusCustomFeesValidator customFeesValidator ;
7575
76+ /**
77+ * The possible types of updates to a topic.
78+ */
79+ private enum UpdateType {
80+ NOOP ,
81+ EXPIRY_ONLY ,
82+ ADMIN_MANAGED_ONLY ,
83+ FEE_SCHEDULE_MANAGED_ONLY ,
84+ ADMIN_AND_FEE_SCHEDULE_MANAGED ,
85+ }
86+
7687 /**
7788 * Default constructor for injection.
7889 * @param customFeesValidator custom fees validator
@@ -110,43 +121,33 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx
110121 mustExist (topic , INVALID_TOPIC_ID );
111122 validateFalsePreCheck (topic .deleted (), INVALID_TOPIC_ID );
112123
113- // Extending the expiry is the *only* update operation permitted without an admin key. So if that is the
114- // only thing this transaction is doing, then we don't need to worry about checking any additional keys.
115- if (onlyExtendsExpiry (op )) {
116- return ;
124+ final var updateType = classifyUpdate (op );
125+ switch (updateType ) {
126+ case NOOP , EXPIRY_ONLY -> {
127+ return ;
128+ }
129+ case ADMIN_MANAGED_ONLY -> context .requireKeyOrThrow (topic .adminKey (), UNAUTHORIZED );
130+ case FEE_SCHEDULE_MANAGED_ONLY -> {
131+ validateTruePreCheck (topic .hasFeeScheduleKey (), FEE_SCHEDULE_KEY_NOT_SET );
132+ context .requireKey (topic .feeScheduleKeyOrThrow ());
133+ }
134+ case ADMIN_AND_FEE_SCHEDULE_MANAGED -> {
135+ context .requireKeyOrThrow (topic .adminKey (), UNAUTHORIZED );
136+ validateTruePreCheck (topic .hasFeeScheduleKey (), FEE_SCHEDULE_KEY_NOT_SET );
137+ context .requireKey (topic .feeScheduleKeyOrThrow ());
138+ }
117139 }
118-
119- // Any other modifications on this topic require the admin key.
120- context .requireKeyOrThrow (topic .adminKey (), UNAUTHORIZED );
121-
122140 // If the transaction is setting a new admin key, then the transaction must also be signed by that new key
123141 if (op .hasAdminKey ()) {
124142 context .requireKey (op .adminKeyOrThrow ());
125143 }
126-
127- // If the transaction is setting a new account for auto-renewals, then that account must also
128- // have signed the transaction
144+ // New auto-renew accounts must also sign
129145 if (op .hasAutoRenewAccount ()) {
130146 final var autoRenewAccountID = op .autoRenewAccountOrThrow ();
131147 if (!designatesAccountRemoval (autoRenewAccountID )) {
132148 context .requireKeyOrThrow (autoRenewAccountID , INVALID_AUTORENEW_ACCOUNT );
133149 }
134150 }
135-
136- // If we change the custom fees the topic needs to have a fee schedule key, and it needs to sign the transaction
137- if (op .hasCustomFees ()) {
138- validateTruePreCheck (topic .hasFeeScheduleKey (), FEE_SCHEDULE_KEY_NOT_SET );
139- context .requireKey (topic .feeScheduleKey ());
140- }
141- }
142-
143- private boolean onlyExtendsExpiry (@ NonNull final ConsensusUpdateTopicTransactionBody op ) {
144- return op .hasExpirationTime ()
145- && !op .hasMemo ()
146- && !op .hasAdminKey ()
147- && !op .hasSubmitKey ()
148- && !op .hasAutoRenewPeriod ()
149- && !op .hasAutoRenewAccount ();
150151 }
151152
152153 /**
@@ -163,19 +164,20 @@ public void handle(@NonNull final HandleContext handleContext) {
163164 final var op = txn .consensusUpdateTopicOrThrow ();
164165
165166 final var topicStore = handleContext .storeFactory ().writableStore (WritableTopicStore .class );
166- final var topic = topicStore .getTopic (op .topicIDOrElse (TopicID .DEFAULT ));
167+ final var topic = requireNonNull ( topicStore .getTopic (op .topicIDOrElse (TopicID .DEFAULT ) ));
167168 // preHandle already checks for topic existence, so topic should never be null.
168169
169- // First validate this topic is mutable; and the pending mutations are allowed
170- if (wantsToMutateNonExpiryField (op )) {
171- validateTrue (topic .hasAdminKey (), UNAUTHORIZED );
172- final var opRemovesAutoRenewId =
173- op .hasAutoRenewAccount () && designatesAccountRemoval (op .autoRenewAccount ());
174- if (!opRemovesAutoRenewId && topic .hasAutoRenewAccountId ()) {
175- validateFalse (
176- !topic .hasAdminKey () || (op .hasAdminKey () && isEmpty (op .adminKey ())),
177- AUTORENEW_ACCOUNT_NOT_ALLOWED );
178- }
170+ // For backward compatibility, don't allow removing the admin key if the topic will still have an auto-renew
171+ // account (this is nonstandard relative to other entities, but must be preserved now)
172+ final boolean removesAdminKey = op .hasAdminKey () && isEmpty (op .adminKey ());
173+ if (removesAdminKey ) {
174+ final boolean addsAutoRenewAccount =
175+ op .hasAutoRenewAccount () && !designatesAccountRemoval (op .autoRenewAccountOrThrow ());
176+ final boolean removesAutoRenewAccount =
177+ op .hasAutoRenewAccount () && designatesAccountRemoval (op .autoRenewAccountOrThrow ());
178+ final boolean endsWithAutoRenewAccount =
179+ addsAutoRenewAccount || (topic .hasAutoRenewAccountId () && !removesAutoRenewAccount );
180+ validateFalse (endsWithAutoRenewAccount , AUTORENEW_ACCOUNT_NOT_ALLOWED );
179181 }
180182
181183 validateMaybeNewAttributes (handleContext , op , topic );
@@ -352,12 +354,11 @@ private void validateMaybeFeeExemptKeyList(
352354 final var configuration = handleContext .configuration ();
353355 final var topicConfig = configuration .getConfigData (TopicsConfig .class );
354356
357+ final var keys = op .feeExemptKeyListOrThrow ().keys ();
355358 validateTrue (
356- op . feeExemptKeyList (). keys () .size () <= topicConfig .maxEntriesForFeeExemptKeyList (),
359+ keys .size () <= topicConfig .maxEntriesForFeeExemptKeyList (),
357360 MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED );
358- op .feeExemptKeyList ()
359- .keys ()
360- .forEach (key -> attributeValidator .validateKey (key , INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST ));
361+ keys .forEach (key -> attributeValidator .validateKey (key , INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST ));
361362 }
362363
363364 private void validateMaybeCustomFees (
@@ -378,18 +379,6 @@ private void validateMaybeCustomFees(
378379 accountStore , tokenRelStore , tokenStore , op .customFees ().fees (), handleContext .expiryValidator ());
379380 }
380381
381- /**
382- * @param op the transaction body of consensus update operation
383- * @return {@code true} if the operation wants to update a non-expiry field, {@code false} otherwise.
384- */
385- public static boolean wantsToMutateNonExpiryField (@ NonNull final ConsensusUpdateTopicTransactionBody op ) {
386- return op .hasMemo ()
387- || op .hasAdminKey ()
388- || op .hasSubmitKey ()
389- || op .hasAutoRenewPeriod ()
390- || op .hasAutoRenewAccount ();
391- }
392-
393382 private boolean designatesAccountRemoval (AccountID id ) {
394383 return id .hasAccountNum () && id .accountNum () == 0 && id .alias () == null ;
395384 }
@@ -416,4 +405,33 @@ private FeeData usageGivenExplicit(
416405 }
417406 return getConsensusUpdateTopicFee (protoTxnBody , rbsIncrease , sigUsage );
418407 }
408+
409+ /**
410+ * Examines the fields set in the transaction body to determine the type of update.
411+ * @param op the transaction body
412+ * @return the type of update
413+ */
414+ private UpdateType classifyUpdate (@ NonNull final ConsensusUpdateTopicTransactionBody op ) {
415+ final boolean hasExpiry = op .hasExpirationTime ();
416+ final boolean hasFeeScheduleManaged = op .hasCustomFees ();
417+ // No matter new fields are added in ConsensusUpdateTopicTransactionBody, we *default* to requiring
418+ // admin key signature to authorize their updates...only expiration time (no sig required) and custom
419+ // fees (fee schedule key required) are exceptions to the rule
420+ final boolean hasAdminManaged = !op .equals (ConsensusUpdateTopicTransactionBody .newBuilder ()
421+ .topicID (op .topicID ())
422+ .expirationTime (op .expirationTime ())
423+ .customFees (op .customFees ())
424+ .build ());
425+ if (hasFeeScheduleManaged || hasAdminManaged ) {
426+ if (hasFeeScheduleManaged && hasAdminManaged ) {
427+ return UpdateType .ADMIN_AND_FEE_SCHEDULE_MANAGED ;
428+ } else if (hasFeeScheduleManaged ) {
429+ return UpdateType .FEE_SCHEDULE_MANAGED_ONLY ;
430+ } else {
431+ return UpdateType .ADMIN_MANAGED_ONLY ;
432+ }
433+ } else {
434+ return hasExpiry ? UpdateType .EXPIRY_ONLY : UpdateType .NOOP ;
435+ }
436+ }
419437}
0 commit comments