diff --git a/examples/README.md b/examples/README.md index 210c253c8..f792f772a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -53,6 +53,7 @@ * [Custom fees (exempt)](../examples/src/main/java/ExemptCustomFeesExample.java) * [NFT Allowances](../examples/src/main/java/NftAddRemoveAllowancesExample.java) * [Zero token operations](../examples/src/main/java/ZeroTokenOperationsExample.java) +* [Change Or Remove Existing Keys From A Token (HIP-540)](../examples/src/main/java/ChangeRemoveTokenKeys.java) ### File Service * [Create a file](../examples/src/main/java/CreateFileExample.java) diff --git a/examples/src/main/java/ChangeRemoveTokenKeys.java b/examples/src/main/java/ChangeRemoveTokenKeys.java new file mode 100644 index 000000000..7004ba744 --- /dev/null +++ b/examples/src/main/java/ChangeRemoveTokenKeys.java @@ -0,0 +1,147 @@ +import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.Client; +import com.hedera.hashgraph.sdk.KeyList; +import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.PublicKey; +import com.hedera.hashgraph.sdk.TokenCreateTransaction; +import com.hedera.hashgraph.sdk.TokenInfoQuery; +import com.hedera.hashgraph.sdk.TokenKeyValidation; +import com.hedera.hashgraph.sdk.TokenType; +import com.hedera.hashgraph.sdk.TokenUpdateTransaction; +import io.github.cdimascio.dotenv.Dotenv; +import java.util.Objects; + +public class ChangeRemoveTokenKeys { + + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + + private static final PrivateKey OPERATOR_KEY = PrivateKey.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY"))); + + // HEDERA_NETWORK defaults to testnet if not specified in dotenv + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + private ChangeRemoveTokenKeys() { + } + + public static void main(String[] args) throws Exception { + Client client = ClientHelper.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for + // by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + // Admin, Supply, Wipe keys + var adminKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var newSupplyKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + + // This HIP introduces ability to remove lower-privilege keys (Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata) from a Token: + // - using an update with the empty KeyList; + var emptyKeyList = new KeyList(); + + // create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Example NFT") + .setTokenSymbol("ENFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(OPERATOR_ID) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .freezeWith(client) + .sign(adminKey) + .execute(client) + .getReceipt(client) + .tokenId + ); + + var tokenInfoBefore = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(client); + + System.out.println("Admin Key:" + tokenInfoBefore.adminKey); + System.out.println("Supply Key:" + tokenInfoBefore.supplyKey); + System.out.println("Wipe Key:" + tokenInfoBefore.wipeKey); + + System.out.println("---"); + System.out.println("Removing Wipe Key..."); + + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) // it is by default, but we set explicitly for illustration + .freezeWith(client) + .sign(adminKey) + .execute(client) + .getReceipt(client); + + var tokenInfoAfterWipeKeyRemoval = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(client); + + System.out.println("Wipe Key (after removal):" + tokenInfoAfterWipeKeyRemoval.wipeKey); + + System.out.println("---"); + System.out.println("Removing Admin Key..."); + + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setAdminKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(client) + .sign(adminKey) + .execute(client) + .getReceipt(client); + + var tokenInfoAfterAdminKeyRemoval = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(client); + + System.out.println("Admin Key (after removal):" + tokenInfoAfterAdminKeyRemoval.adminKey); + + System.out.println("---"); + System.out.println("Updating Supply Key..."); + + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(newSupplyKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(client) + .sign(supplyKey) + .sign(newSupplyKey) + .execute(client) + .getReceipt(client); + + var tokenInfoAfterSupplyKeyUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(client); + + System.out.println("Supply Key (after update):" + tokenInfoAfterSupplyKeyUpdate.supplyKey); + + System.out.println("---"); + System.out.println("Removing Supply Key..."); + + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(client) + .sign(newSupplyKey) + .execute(client) + .getReceipt(client); + + var tokenInfoAfterSupplyKeyRemoval = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(client); + + var supplyKeyAfterRemoval = (PublicKey) tokenInfoAfterSupplyKeyRemoval.supplyKey; + + System.out.println("Supply Key (after removal):" + supplyKeyAfterRemoval.toStringRaw()); + + client.close(); + } +} diff --git a/sdk/src/integrationTest/java/TokenUpdateIntegrationTest.java b/sdk/src/integrationTest/java/TokenUpdateIntegrationTest.java index 7862c2cda..9079df996 100644 --- a/sdk/src/integrationTest/java/TokenUpdateIntegrationTest.java +++ b/sdk/src/integrationTest/java/TokenUpdateIntegrationTest.java @@ -2,11 +2,15 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import com.google.errorprone.annotations.Var; +import com.hedera.hashgraph.sdk.KeyList; +import com.hedera.hashgraph.sdk.PrecheckStatusException; import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.PublicKey; import com.hedera.hashgraph.sdk.ReceiptStatusException; import com.hedera.hashgraph.sdk.Status; import com.hedera.hashgraph.sdk.TokenCreateTransaction; import com.hedera.hashgraph.sdk.TokenInfoQuery; +import com.hedera.hashgraph.sdk.TokenKeyValidation; import com.hedera.hashgraph.sdk.TokenType; import com.hedera.hashgraph.sdk.TokenUpdateTransaction; import java.util.Objects; @@ -684,4 +688,1798 @@ void cannotUpdateNonFungibleTokenMetadataWhenMetadataKeyNotSet() throws Exceptio testEnv.close(tokenId); } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can make a token immutable when updating keys to an empty KeyList, signing with an Admin Key, and setting the key verification mode to NO_VALIDATION") + void canMakeTokenImmutableWhenUpdatingKeysToEmptyKeyListSigningWithAdminKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin, Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var adminKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + var emptyKeyList = new KeyList(); + + // Make a token immutable by removing all of its keys when updating them to an empty KeyList, + // signing with an Admin Key, and setting the key verification mode to NO_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(emptyKeyList) + .setKycKey(emptyKeyList) + .setFreezeKey(emptyKeyList) + .setPauseKey(emptyKeyList) + .setSupplyKey(emptyKeyList) + .setFeeScheduleKey(emptyKeyList) + .setMetadataKey(emptyKeyList) + .setAdminKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.adminKey).isNull(); + assertThat(tokenInfoAfterUpdate.wipeKey).isNull(); + assertThat(tokenInfoAfterUpdate.kycKey).isNull(); + assertThat(tokenInfoAfterUpdate.freezeKey).isNull(); + assertThat(tokenInfoAfterUpdate.pauseKey).isNull(); + assertThat(tokenInfoAfterUpdate.supplyKey).isNull(); + assertThat(tokenInfoAfterUpdate.feeScheduleKey).isNull(); + assertThat(tokenInfoAfterUpdate.metadataKey).isNull(); + + testEnv.close(tokenId); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can remove all of token’s lower-privilege keys when updating keys to an empty KeyList, signing with an Admin Key, and setting the key verification mode to FULL_VALIDATION") + void canRemoveAllLowerPrivilegeKeysWhenUpdatingKeysToEmptyKeyListSigningWithAdminKeyWithKeyVerificationSetToFullValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin, Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var adminKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + var emptyKeyList = new KeyList(); + + // Remove all of token’s lower-privilege keys when updating them to an empty KeyList, + // signing with an Admin Key, and setting the key verification mode to FULL_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(emptyKeyList) + .setKycKey(emptyKeyList) + .setFreezeKey(emptyKeyList) + .setPauseKey(emptyKeyList) + .setSupplyKey(emptyKeyList) + .setFeeScheduleKey(emptyKeyList) + .setMetadataKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.wipeKey).isNull(); + assertThat(tokenInfoAfterUpdate.kycKey).isNull(); + assertThat(tokenInfoAfterUpdate.freezeKey).isNull(); + assertThat(tokenInfoAfterUpdate.pauseKey).isNull(); + assertThat(tokenInfoAfterUpdate.supplyKey).isNull(); + assertThat(tokenInfoAfterUpdate.feeScheduleKey).isNull(); + assertThat(tokenInfoAfterUpdate.metadataKey).isNull(); + + testEnv.close(tokenId); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can update all of token’s lower-privilege keys to an unusable key (i.e. all-zeros key), when signing with an Admin Key, and setting the key verification mode to FULL_VALIDATION, and then revert previous keys") + void canUpdateAllLowerPrivilegeKeysToUnusableKeyWhenSigningWithAdminKeyWithKeyVerificationSetToFullValidationAndThenRevertPreviousKeys() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin, Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var adminKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys to an unusable key (i.e., all-zeros key), + // signing with an Admin Key, and setting the key verification mode to FULL_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(PublicKey.unusableKey()) + .setKycKey(PublicKey.unusableKey()) + .setFreezeKey(PublicKey.unusableKey()) + .setPauseKey(PublicKey.unusableKey()) + .setSupplyKey(PublicKey.unusableKey()) + .setFeeScheduleKey(PublicKey.unusableKey()) + .setMetadataKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.wipeKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.kycKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.freezeKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.pauseKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.supplyKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.feeScheduleKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.metadataKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + + // Set all lower-privilege keys back by signing with an Admin Key, + // and setting key verification mode to NO_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterRevert = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterRevert.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoAfterRevert.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + testEnv.close(tokenId); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can update all of token’s lower-privilege keys when signing with an Admin Key and new respective lower-privilege key, and setting key verification mode to FULL_VALIDATION") + void canUpdateAllLowerPrivilegeKeysWhenSigningWithAdminKeyAndNewLowerPrivilegeKeyWithKeyVerificationSetToFullValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin, Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var adminKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // New Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var newWipeKey = PrivateKey.generateED25519(); + var newKycKey = PrivateKey.generateED25519(); + var newFreezeKey = PrivateKey.generateED25519(); + var newPauseKey = PrivateKey.generateED25519(); + var newSupplyKey = PrivateKey.generateED25519(); + var newFeeScheduleKey = PrivateKey.generateED25519(); + var newMetadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys when signing with an Admin Key and new respective lower-privilege key, + // and setting key verification mode to FULL_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(newWipeKey.getPublicKey()) + .setKycKey(newKycKey.getPublicKey()) + .setFreezeKey(newFreezeKey.getPublicKey()) + .setPauseKey(newPauseKey.getPublicKey()) + .setSupplyKey(newSupplyKey.getPublicKey()) + .setFeeScheduleKey(newFeeScheduleKey.getPublicKey()) + .setMetadataKey(newMetadataKey.getPublicKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(adminKey) + .sign(newWipeKey) + .sign(newKycKey) + .sign(newFreezeKey) + .sign(newPauseKey) + .sign(newSupplyKey) + .sign(newFeeScheduleKey) + .sign(newMetadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.wipeKey.toString()).isEqualTo(newWipeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.kycKey.toString()).isEqualTo(newKycKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.freezeKey.toString()).isEqualTo(newFreezeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.pauseKey.toString()).isEqualTo(newPauseKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.supplyKey.toString()).isEqualTo(newSupplyKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.feeScheduleKey.toString()).isEqualTo(newFeeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.metadataKey.toString()).isEqualTo(newMetadataKey.getPublicKey().toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot make a token immutable when updating keys to an empty KeyList, signing with a key that is different from an Admin Key, and setting the key verification mode to NO_VALIDATION") + void cannotMakeTokenImmutableWhenUpdatingKeysToEmptyKeyListSigningWithDifferentKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin, Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var adminKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + var emptyKeyList = new KeyList(); + + // Make the token immutable when updating all of its keys to an empty KeyList + // (trying to remove keys one by one to check all errors), + // signing with a key that is different from an Admin Key (implicitly with an operator key), + // and setting the key verification mode to NO_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setAdminKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot make a token immutable when updating keys to an unusable key (i.e. all-zeros key), signing with a key that is different from an Admin Key, and setting the key verification mode to NO_VALIDATION") + void cannotMakeTokenImmutableWhenUpdatingKeysToUnusableKeySigningWithDifferentKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin, Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var adminKey = PrivateKey.generateED25519(); + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Make the token immutable when updating all of its keys to an unusable key (i.e. all-zeros key) + // (trying to remove keys one by one to check all errors), + // signing with a key that is different from an Admin Key (implicitly with an operator key), + // and setting the key verification mode to NO_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setAdminKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot update the Admin Key to an unusable key (i.e. all-zeros key), signing with an Admin Key, and setting the key verification mode to NO_VALIDATION") + void cannotUpdateAdminKeyToUnusableKeySigningWithAdminKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Admin and supply keys + var adminKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setAdminKey(adminKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.adminKey.toString()).isEqualTo(adminKey.getPublicKey().toString()); + + // Update the Admin Key to an unusable key (i.e., all-zeros key), + // signing with an Admin Key, and setting the key verification mode to NO_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setAdminKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(adminKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + testEnv.close(tokenId); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can update all of token’s lower-privilege keys to an unusable key (i.e. all-zeros key), when signing with a respective lower-privilege key, and setting the key verification mode to NO_VALIDATION") + void canUpdateAllLowerPrivilegeKeysToUnusableKeyWhenSigningWithRespectiveLowerPrivilegeKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys to an unusable key (i.e., all-zeros key), + // when signing with a respective lower-privilege key, + // and setting the key verification mode to NO_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(PublicKey.unusableKey()) + .setKycKey(PublicKey.unusableKey()) + .setFreezeKey(PublicKey.unusableKey()) + .setPauseKey(PublicKey.unusableKey()) + .setSupplyKey(PublicKey.unusableKey()) + .setFeeScheduleKey(PublicKey.unusableKey()) + .setMetadataKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .sign(kycKey) + .sign(freezeKey) + .sign(pauseKey) + .sign(supplyKey) + .sign(feeScheduleKey) + .sign(metadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.wipeKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.kycKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.freezeKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.pauseKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.supplyKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.feeScheduleKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + assertThat(tokenInfoAfterUpdate.metadataKey.toString()).isEqualTo(PublicKey.unusableKey().toString()); + + testEnv.close(tokenId); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can update all of token’s lower-privilege keys when signing with an old lower-privilege key and with a new lower-privilege key, and setting key verification mode to FULL_VALIDATION") + void canUpdateAllLowerPrivilegeKeysWhenSigningWithOldLowerPrivilegeKeyAndNewLowerPrivilegeKeyWithKeyVerificationSetToFulValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // New Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var newWipeKey = PrivateKey.generateED25519(); + var newKycKey = PrivateKey.generateED25519(); + var newFreezeKey = PrivateKey.generateED25519(); + var newPauseKey = PrivateKey.generateED25519(); + var newSupplyKey = PrivateKey.generateED25519(); + var newFeeScheduleKey = PrivateKey.generateED25519(); + var newMetadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys when signing with an old respective lower-privilege key, + // and setting key verification mode to NO_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(newWipeKey.getPublicKey()) + .setKycKey(newKycKey.getPublicKey()) + .setFreezeKey(newFreezeKey.getPublicKey()) + .setPauseKey(newPauseKey.getPublicKey()) + .setSupplyKey(newSupplyKey.getPublicKey()) + .setFeeScheduleKey(newFeeScheduleKey.getPublicKey()) + .setMetadataKey(newMetadataKey.getPublicKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .sign(newWipeKey) + .sign(kycKey) + .sign(newKycKey) + .sign(freezeKey) + .sign(newFreezeKey) + .sign(pauseKey) + .sign(newPauseKey) + .sign(supplyKey) + .sign(newSupplyKey) + .sign(feeScheduleKey) + .sign(newFeeScheduleKey) + .sign(metadataKey) + .sign(newMetadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.wipeKey.toString()).isEqualTo(newWipeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.kycKey.toString()).isEqualTo(newKycKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.freezeKey.toString()).isEqualTo(newFreezeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.pauseKey.toString()).isEqualTo(newPauseKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.supplyKey.toString()).isEqualTo(newSupplyKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.feeScheduleKey.toString()).isEqualTo(newFeeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.metadataKey.toString()).isEqualTo(newMetadataKey.getPublicKey().toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Can update all of token’s lower-privilege keys when signing ONLY with an old lower-privilege key, and setting key verification mode to NO_VALIDATION") + void canUpdateAllLowerPrivilegeKeysWhenSigningOnlyWithOldLowerPrivilegeKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // New Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var newWipeKey = PrivateKey.generateED25519(); + var newKycKey = PrivateKey.generateED25519(); + var newFreezeKey = PrivateKey.generateED25519(); + var newPauseKey = PrivateKey.generateED25519(); + var newSupplyKey = PrivateKey.generateED25519(); + var newFeeScheduleKey = PrivateKey.generateED25519(); + var newMetadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys when signing with an old respective lower-privilege key, + // and setting key verification mode to NO_VALIDATION + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(newWipeKey.getPublicKey()) + .setKycKey(newKycKey.getPublicKey()) + .setFreezeKey(newFreezeKey.getPublicKey()) + .setPauseKey(newPauseKey.getPublicKey()) + .setSupplyKey(newSupplyKey.getPublicKey()) + .setFeeScheduleKey(newFeeScheduleKey.getPublicKey()) + .setMetadataKey(newMetadataKey.getPublicKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .sign(kycKey) + .sign(freezeKey) + .sign(pauseKey) + .sign(supplyKey) + .sign(feeScheduleKey) + .sign(metadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var tokenInfoAfterUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoAfterUpdate.wipeKey.toString()).isEqualTo(newWipeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.kycKey.toString()).isEqualTo(newKycKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.freezeKey.toString()).isEqualTo(newFreezeKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.pauseKey.toString()).isEqualTo(newPauseKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.supplyKey.toString()).isEqualTo(newSupplyKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.feeScheduleKey.toString()).isEqualTo(newFeeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoAfterUpdate.metadataKey.toString()).isEqualTo(newMetadataKey.getPublicKey().toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot remove all of token’s lower-privilege keys when updating them to an empty KeyList, signing with a respective lower-privilege key, and setting the key verification mode to NO_VALIDATION") + void cannotRemoveAllLowerPrivilegeKeysWhenUpdatingKeysToEmptyKeyListSigningWithRespectiveLowerPrivilegeKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + var emptyKeyList = new KeyList(); + + // Remove all of token’s lower-privilege keys + // when updating them to an empty KeyList (trying to remove keys one by one to check all errors), + // signing with a respective lower-privilege key, + // and setting the key verification mode to NO_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(kycKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(freezeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(pauseKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(supplyKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(feeScheduleKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(emptyKeyList) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .freezeWith(testEnv.client) + .sign(metadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.TOKEN_IS_IMMUTABLE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot update all of token’s lower-privilege keys to an unusable key (i.e. all-zeros key), when signing with a key that is different from a respective lower-privilege key, and setting the key verification mode to NO_VALIDATION") + void cannotUpdateAllLowerPrivilegeKeysToUnusableKeyWhenSigningWithDifferentKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys to an unusable key (i.e. all-zeros key) + // (trying to remove keys one by one to check all errors), + // signing with a key that is different from a respective lower-privilege key (implicitly with an operator key), + // and setting the key verification mode to NO_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot update all of token’s lower-privilege keys to an unusable key (i.e. all-zeros key), when signing ONLY with an old respective lower-privilege key, and setting the key verification mode to FULL_VALIDATION") + void cannotUpdateAllLowerPrivilegeKeysToUnusableKeyWhenSigningOnlyWithOldRespectiveLowerPrivilegeKeyWithKeyVerificationSetToFullValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys to an unusable key (i.e., all-zeros key) + // (trying to remove keys one by one to check all errors), + // signing ONLY with an old respective lower-privilege key, + // and setting the key verification mode to FULL_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(kycKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(freezeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(pauseKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(supplyKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(feeScheduleKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(metadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot update all of token’s lower-privilege keys to an unusable key (i.e. all-zeros key), when signing with an old respective lower-privilege key and new respective lower-privilege key, and setting the key verification mode to FULL_VALIDATION") + void cannotUpdateAllLowerPrivilegeKeysToUnusableKeyWhenSigningWithOldRespectiveLowerPrivilegeKeyAndNewRespectiveLowerPrivilegeKeyWithKeyVerificationSetToFullValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // New Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var newWipeKey = PrivateKey.generateED25519(); + var newKycKey = PrivateKey.generateED25519(); + var newFreezeKey = PrivateKey.generateED25519(); + var newPauseKey = PrivateKey.generateED25519(); + var newSupplyKey = PrivateKey.generateED25519(); + var newFeeScheduleKey = PrivateKey.generateED25519(); + var newMetadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys to an unusable key (i.e., all-zeros key) + // (trying to remove keys one by one to check all errors), + // signing with an old respective lower-privilege key and new respective lower-privilege key, + // and setting the key verification mode to FULL_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .sign(newWipeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(kycKey) + .sign(newKycKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(freezeKey) + .sign(newFreezeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(pauseKey) + .sign(newPauseKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(supplyKey) + .sign(newSupplyKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(feeScheduleKey) + .sign(newFeeScheduleKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(PublicKey.unusableKey()) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(metadataKey) + .sign(newMetadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot update all of token’s lower-privilege keys, when signing ONLY with an old respective lower-privilege key, and setting the key verification mode to FULL_VALIDATION") + void cannotUpdateAllLowerPrivilegeKeysWhenSigningOnlyWithOldRespectiveLowerPrivilegeKeyWithKeyVerificationSetToFullValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // New Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var newWipeKey = PrivateKey.generateED25519(); + var newKycKey = PrivateKey.generateED25519(); + var newFreezeKey = PrivateKey.generateED25519(); + var newPauseKey = PrivateKey.generateED25519(); + var newSupplyKey = PrivateKey.generateED25519(); + var newFeeScheduleKey = PrivateKey.generateED25519(); + var newMetadataKey = PrivateKey.generateED25519(); + + // Create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // Update all of token’s lower-privilege keys + // (trying to update keys one by one to check all errors), + // signing ONLY with an old respective lower-privilege key, + // and setting the key verification mode to FULL_VALIDATION + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(newWipeKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(newKycKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(kycKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(newFreezeKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(freezeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(newPauseKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(pauseKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(newSupplyKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(supplyKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(newFeeScheduleKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(feeScheduleKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + + assertThatExceptionOfType(ReceiptStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(newMetadataKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(metadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SIGNATURE.toString()); + } + + /** + * @notice E2E-HIP-540 + * @url https://hips.hedera.com/hip/hip-540 + */ + @Test + @DisplayName("Cannot update all of token’s lower-privilege keys when updating them to a keys with an invalid structure and signing with an old respective lower-privilege and setting key verification mode to NO_VALIDATION") + void cannotUpdateAllLowerPrivilegeKeysWhenUpdatingKeysToStructurallyInvalidKeysSigningOnlyWithOldRespectiveLowerPrivilegeKeyWithKeyVerificationSetToNoValidation() throws Exception { + var testEnv = new IntegrationTestEnv(1).useThrowawayAccount(); + + // Wipe, KYC, Freeze, Pause, Supply, Fee Schedule, Metadata keys + var wipeKey = PrivateKey.generateED25519(); + var kycKey = PrivateKey.generateED25519(); + var freezeKey = PrivateKey.generateED25519(); + var pauseKey = PrivateKey.generateED25519(); + var supplyKey = PrivateKey.generateED25519(); + var feeScheduleKey = PrivateKey.generateED25519(); + var metadataKey = PrivateKey.generateED25519(); + + // create a non-fungible token + var tokenId = Objects.requireNonNull( + new TokenCreateTransaction() + .setTokenName("Test NFT") + .setTokenSymbol("TNFT") + .setTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(testEnv.operatorId) + .setWipeKey(wipeKey.getPublicKey()) + .setKycKey(kycKey.getPublicKey()) + .setFreezeKey(freezeKey.getPublicKey()) + .setPauseKey(pauseKey.getPublicKey()) + .setSupplyKey(supplyKey.getPublicKey()) + .setFeeScheduleKey(feeScheduleKey.getPublicKey()) + .setMetadataKey(metadataKey.getPublicKey()) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId + ); + + var tokenInfoBeforeUpdate = new TokenInfoQuery() + .setTokenId(tokenId) + .execute(testEnv.client); + + assertThat(tokenInfoBeforeUpdate.wipeKey.toString()).isEqualTo(wipeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.kycKey.toString()).isEqualTo(kycKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.freezeKey.toString()).isEqualTo(freezeKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.pauseKey.toString()).isEqualTo(pauseKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.supplyKey.toString()).isEqualTo(supplyKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.feeScheduleKey.toString()).isEqualTo(feeScheduleKey.getPublicKey().toString()); + assertThat(tokenInfoBeforeUpdate.metadataKey.toString()).isEqualTo(metadataKey.getPublicKey().toString()); + + // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long + var structurallyInvalidKey = PublicKey.fromString("000000000000000000000000000000000000000000000000000000000000000000"); + + // update all of token’s lower-privilege keys + // to a structurally invalid key (trying to update keys one by one to check all errors), + // signing with an old respective lower-privilege + // and setting key verification mode to NO_VALIDATION + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setWipeKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(wipeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_WIPE_KEY.toString()); + + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setKycKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(kycKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_KYC_KEY.toString()); + + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFreezeKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(freezeKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_FREEZE_KEY.toString()); + + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setPauseKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(pauseKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_PAUSE_KEY.toString()); + + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setSupplyKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(supplyKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_SUPPLY_KEY.toString()); + + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setFeeScheduleKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(feeScheduleKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_CUSTOM_FEE_SCHEDULE_KEY.toString()); + + assertThatExceptionOfType(PrecheckStatusException.class).isThrownBy(() -> { + new TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadataKey(structurallyInvalidKey) + .setKeyVerificationMode(TokenKeyValidation.FULL_VALIDATION) + .freezeWith(testEnv.client) + .sign(metadataKey) + .execute(testEnv.client) + .getReceipt(testEnv.client); + }).withMessageContaining(Status.INVALID_METADATA_KEY.toString()); + } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java index 843a7e582..a35e151f2 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java @@ -293,4 +293,15 @@ public AccountId toAccountId(@Nonnegative long shard, @Nonnegative long realm) { * @return the EVM address */ public abstract EvmAddress toEvmAddress(); + + /** + * Returns an "unusable" public key. + * “Unusable” refers to a key such as an Ed25519 0x00000... public key, + * since it is (presumably) impossible to find the 32-byte string whose SHA-512 hash begins with 32 bytes of zeros. + * + * @return The "unusable" key + */ + public static PublicKey unusableKey() { + return PublicKey.fromStringED25519("0000000000000000000000000000000000000000000000000000000000000000"); + } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenKeyValidation.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenKeyValidation.java new file mode 100644 index 000000000..6dd3f377b --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenKeyValidation.java @@ -0,0 +1,48 @@ +package com.hedera.hashgraph.sdk; + +/** + * Types of validation strategies for token keys. + * + */ +public enum TokenKeyValidation { + /** + * Currently the default behaviour. It will perform all token key validations. + */ + FULL_VALIDATION(com.hedera.hashgraph.sdk.proto.TokenKeyValidation.FULL_VALIDATION), + + /** + * Perform no validations at all for all passed token keys. + */ + NO_VALIDATION(com.hedera.hashgraph.sdk.proto.TokenKeyValidation.NO_VALIDATION); + + final com.hedera.hashgraph.sdk.proto.TokenKeyValidation code; + + /** + * Constructor. + * + * @param code the token key validation + */ + TokenKeyValidation(com.hedera.hashgraph.sdk.proto.TokenKeyValidation code) { + this.code = code; + } + + static TokenKeyValidation valueOf(com.hedera.hashgraph.sdk.proto.TokenKeyValidation code) { + return switch (code) { + case FULL_VALIDATION -> FULL_VALIDATION; + case NO_VALIDATION -> NO_VALIDATION; + default -> throw new IllegalStateException("(BUG) unhandled TokenKeyValidation"); + }; + } + + @Override + public String toString() { + return switch (this) { + case FULL_VALIDATION -> "FULL_VALIDATION"; + case NO_VALIDATION -> "NO_VALIDATION"; + }; + } + + public com.hedera.hashgraph.sdk.proto.TokenKeyValidation toProtobuf() { + return this.code; + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenUpdateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenUpdateTransaction.java index 5ad753c9b..0505d4af8 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenUpdateTransaction.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/TokenUpdateTransaction.java @@ -161,6 +161,11 @@ public class TokenUpdateTransaction extends Transaction * Metadata of the created token definition */ private byte[] tokenMetadata = null; + /** + * Determines whether the system should check the validity of the passed keys for update. + * Defaults to FULL_VALIDATION; + */ + private TokenKeyValidation tokenKeyVerificationMode = TokenKeyValidation.FULL_VALIDATION; /** * Constructor. */ @@ -605,6 +610,28 @@ public TokenUpdateTransaction setTokenMetadata(byte[] tokenMetadata) { return this; } + /** + * Extract the key verification mode + * + * @return the key verification mode + */ + public TokenKeyValidation getKeyVerificationMode() { + return tokenKeyVerificationMode; + } + + /** + * Assign the key verification mode. + * + * @param tokenKeyVerificationMode the key verification mode + * @return {@code this} + */ + public TokenUpdateTransaction setKeyVerificationMode(TokenKeyValidation tokenKeyVerificationMode) { + requireNotFrozen(); + Objects.requireNonNull(tokenKeyVerificationMode); + this.tokenKeyVerificationMode = tokenKeyVerificationMode; + return this; + } + /** * Initialize from the transaction body. */ @@ -657,6 +684,7 @@ void initFromTransactionBody() { if (body.hasMetadata()) { tokenMetadata = body.getMetadata().getValue().toByteArray(); } + tokenKeyVerificationMode = TokenKeyValidation.valueOf(body.getKeyVerificationMode()); } /** @@ -715,6 +743,7 @@ TokenUpdateTransactionBody.Builder build() { if (tokenMetadata != null) { builder.setMetadata(BytesValue.of(ByteString.copyFrom(tokenMetadata))); } + builder.setKeyVerificationMode(tokenKeyVerificationMode.code); return builder; } diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.java index 1ad5457f3..7d241fb36 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.java @@ -101,7 +101,7 @@ private TokenUpdateTransaction spawnTestTransaction() { .setTokenSymbol(testTokenSymbol).setKycKey(testKycKey).setPauseKey(testPauseKey) .setMetadataKey(testMetadataKey).setExpirationTime(validStart).setTreasuryAccountId(testTreasuryAccountId) .setTokenName(testTokenName).setTokenMemo(testTokenMemo).setMaxTransactionFee(new Hbar(1)) - .setTokenMetadata(testMetadata).freeze().sign(unusedPrivateKey); + .setTokenMetadata(testMetadata).setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION).freeze().sign(unusedPrivateKey); } @Test @@ -134,7 +134,8 @@ void constructTokenUpdateTransactionFromTransactionBodyProtobuf() { .setMemo(StringValue.newBuilder().setValue(testTokenMemo).build()) .setFeeScheduleKey(testFeeScheduleKey.toProtobufKey()).setPauseKey(testPauseKey.toProtobufKey()) .setMetadataKey(testMetadataKey.toProtobufKey()) - .setMetadata(BytesValue.of(ByteString.copyFrom(testMetadata))).build(); + .setMetadata(BytesValue.of(ByteString.copyFrom(testMetadata))) + .setKeyVerificationMode(com.hedera.hashgraph.sdk.proto.TokenKeyValidation.NO_VALIDATION).build(); var tx = TransactionBody.newBuilder().setTokenUpdate(transactionBody).build(); var tokenUpdateTransaction = new TokenUpdateTransaction(tx); @@ -157,6 +158,7 @@ void constructTokenUpdateTransactionFromTransactionBodyProtobuf() { assertThat(tokenUpdateTransaction.getPauseKey()).isEqualTo(testPauseKey); assertThat(tokenUpdateTransaction.getMetadataKey()).isEqualTo(testMetadataKey); assertThat(tokenUpdateTransaction.getTokenMetadata()).isEqualTo(testMetadata); + assertThat(tokenUpdateTransaction.getKeyVerificationMode()).isEqualTo(TokenKeyValidation.NO_VALIDATION); } @Test @@ -362,4 +364,16 @@ void getSetMetadataFrozen() { var tx = spawnTestTransaction(); assertThrows(IllegalStateException.class, () -> tx.setTokenMetadata(testMetadata)); } + + @Test + void getSetKeyVerificationMode() { + var tx = spawnTestTransaction(); + assertThat(tx.getKeyVerificationMode()).isEqualTo(TokenKeyValidation.NO_VALIDATION); + } + + @Test + void getSetKeyVerificationModeFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setKeyVerificationMode(TokenKeyValidation.NO_VALIDATION)); + } } diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.snap index 2323b7d3d..59624b9b4 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.snap +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TokenUpdateTransactionTest.snap @@ -1,3 +1,3 @@ com.hedera.hashgraph.sdk.TokenUpdateTransactionTest.shouldSerialize=[ - "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\ntoken_update {\n admin_key {\n ed25519: \"\\332\\207p\\020\\227\\206ns\\360\\335\\224,\\273>\\227\\0063)\\371\\005X\\206!\\261x\\322\\027Yh\\215G\\374\"\n }\n auto_renew_account {\n account_num: 8\n realm_num: 8\n shard_num: 8\n }\n expiry {\n seconds: 1554158542\n }\n fee_schedule_key {\n ed25519: \"K\\276\\225\\250m$\\370\\371gs\\261(&\\374\\276\\000\\226\\210\\313\\r\\312\\210\\317\\361\\027\\243\\250\\257P\\303q\\023\"\n }\n freeze_key {\n ed25519: \"=\\355S\\343\\\"3S/=\\204b2L\\321\\023\\253\\276Os!m\\360mT\\241\\034\\266\\221\\301[\\'\\315\"\n }\n kyc_key {\n ed25519: \"\\373\\210\\2637\\337\\327ea{\\3442*\\347\\357\\2053\\326\\036d\\203\\005\\016 \\354HE\\2453\\275\\334\\244\\261\"\n }\n memo {\n value: \"test memo\"\n }\n metadata {\n value: \"\\001\\002\\003\\004\\005\"\n }\n metadata_key {\n ed25519: \"\\024m\\354\\2222\\nnF\\353\\032Cv{\\261\\354\\225\\242\\346\\300%\\032\\260\\335x\\017\\343tt\\324\\272\\304\\025\"\n }\n name: \"test name\"\n pause_key {\n ed25519: \"\\321he\\251\\214\\370\\260\\267\\370\\3727v\\262\\r\\257\\305\\276\\004\\377\\353\\232$\\227r\\a\\203\\316\\231\\036+\\031t\"\n }\n supply_key {\n ed25519: \";\\2218S\\257\\245\\233U\\253\\305\\201\\302\\254\\r6X\\n\\302\\354\\244\\275\\020\\034\\002\\027?\\357\\002\\346w\\335\\325\"\n }\n symbol: \"test symbol\"\n token {\n realm_num: 2\n shard_num: 4\n token_num: 0\n }\n treasury {\n account_num: 7\n realm_num: 7\n shard_num: 7\n }\n wipe_key {\n ed25519: \"R[\\234\\025_\\220+\\221-\\275\\201\\276\\246\\324:\\a}zb\\335\\037\\357\\317\\307}\\351aD\\325\\372\\303\\356\"\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}" -] + "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\ntoken_update {\n admin_key {\n ed25519: \"\\332\\207p\\020\\227\\206ns\\360\\335\\224,\\273>\\227\\0063)\\371\\005X\\206!\\261x\\322\\027Yh\\215G\\374\"\n }\n auto_renew_account {\n account_num: 8\n realm_num: 8\n shard_num: 8\n }\n expiry {\n seconds: 1554158542\n }\n fee_schedule_key {\n ed25519: \"K\\276\\225\\250m$\\370\\371gs\\261(&\\374\\276\\000\\226\\210\\313\\r\\312\\210\\317\\361\\027\\243\\250\\257P\\303q\\023\"\n }\n freeze_key {\n ed25519: \"=\\355S\\343\\\"3S/=\\204b2L\\321\\023\\253\\276Os!m\\360mT\\241\\034\\266\\221\\301[\\'\\315\"\n }\n key_verification_mode: NO_VALIDATION\n key_verification_mode_value: 1\n kyc_key {\n ed25519: \"\\373\\210\\2637\\337\\327ea{\\3442*\\347\\357\\2053\\326\\036d\\203\\005\\016 \\354HE\\2453\\275\\334\\244\\261\"\n }\n memo {\n value: \"test memo\"\n }\n metadata {\n value: \"\\001\\002\\003\\004\\005\"\n }\n metadata_key {\n ed25519: \"\\024m\\354\\2222\\nnF\\353\\032Cv{\\261\\354\\225\\242\\346\\300%\\032\\260\\335x\\017\\343tt\\324\\272\\304\\025\"\n }\n name: \"test name\"\n pause_key {\n ed25519: \"\\321he\\251\\214\\370\\260\\267\\370\\3727v\\262\\r\\257\\305\\276\\004\\377\\353\\232$\\227r\\a\\203\\316\\231\\036+\\031t\"\n }\n supply_key {\n ed25519: \";\\2218S\\257\\245\\233U\\253\\305\\201\\302\\254\\r6X\\n\\302\\354\\244\\275\\020\\034\\002\\027?\\357\\002\\346w\\335\\325\"\n }\n symbol: \"test symbol\"\n token {\n realm_num: 2\n shard_num: 4\n token_num: 0\n }\n treasury {\n account_num: 7\n realm_num: 7\n shard_num: 7\n }\n wipe_key {\n ed25519: \"R[\\234\\025_\\220+\\221-\\275\\201\\276\\246\\324:\\a}zb\\335\\037\\357\\317\\307}\\351aD\\325\\372\\303\\356\"\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}" +] \ No newline at end of file