Skip to content

Commit 7f318b5

Browse files
chore: (0.67) Refine HIP-991 sig requirement checks (#22484)
Signed-off-by: Michael Tinker <[email protected]> Co-authored-by: Michael Tinker <[email protected]>
1 parent ff2fa2e commit 7f318b5

File tree

3 files changed

+269
-98
lines changed

3 files changed

+269
-98
lines changed

hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusUpdateTopicHandlerTest.java

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import static org.assertj.core.api.Assertions.assertThat;
1313
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
1414
import static org.junit.jupiter.api.Assertions.assertEquals;
15-
import static org.junit.jupiter.api.Assertions.assertFalse;
1615
import static org.junit.jupiter.api.Assertions.assertNotNull;
1716
import static org.junit.jupiter.api.Assertions.assertNull;
1817
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -426,50 +425,6 @@ void nothingHappensIfUpdateIsNoop() {
426425
assertEquals(topic, newTopic);
427426
}
428427

429-
@Test
430-
@DisplayName("Check if there is memo update")
431-
void memoMutationsIsNonExpiry() {
432-
final var op = OP_BUILDER.memo("HI").build();
433-
assertTrue(ConsensusUpdateTopicHandler.wantsToMutateNonExpiryField(op));
434-
}
435-
436-
@Test
437-
@DisplayName("Check if there is adminKey update")
438-
void adminKeyMutationIsNonExpiry() {
439-
final var op = OP_BUILDER.adminKey(key).build();
440-
assertTrue(ConsensusUpdateTopicHandler.wantsToMutateNonExpiryField(op));
441-
}
442-
443-
@Test
444-
@DisplayName("Check if there is submitKey update")
445-
void submitKeyMutationIsNonExpiry() {
446-
final var op = OP_BUILDER.submitKey(key).build();
447-
assertTrue(ConsensusUpdateTopicHandler.wantsToMutateNonExpiryField(op));
448-
}
449-
450-
@Test
451-
@DisplayName("Validate Mutate NonExpiryField autoRenewPeriod as expected")
452-
void autoRenewPeriodMutationIsNonExpiry() {
453-
final var autoRenewPeriod = Duration.newBuilder().seconds(123L).build();
454-
final var op = OP_BUILDER.autoRenewPeriod(autoRenewPeriod).build();
455-
assertTrue(ConsensusUpdateTopicHandler.wantsToMutateNonExpiryField(op));
456-
}
457-
458-
@Test
459-
@DisplayName("Check if there is autoRenewAccount update")
460-
void autoRenewAccountMutationIsNonExpiry() {
461-
final var op = OP_BUILDER.autoRenewAccount(autoRenewId).build();
462-
assertTrue(ConsensusUpdateTopicHandler.wantsToMutateNonExpiryField(op));
463-
}
464-
465-
@Test
466-
@DisplayName("Check if there is autoRenewPeriod update")
467-
void expiryMutationIsExpiry() {
468-
final var expiryTime = Timestamp.newBuilder().seconds(123L).build();
469-
final var op = OP_BUILDER.expirationTime(expiryTime).build();
470-
assertFalse(ConsensusUpdateTopicHandler.wantsToMutateNonExpiryField(op));
471-
}
472-
473428
@Test
474429
@DisplayName("payer key as admin key is allowed")
475430
void noneOfFieldsSetHaveNoRequiredKeys() throws PreCheckException {

0 commit comments

Comments
 (0)