com.hedera.node.app.service.token.AliasUtils Maven / Gradle / Ivy
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hedera.node.app.service.token;
import static com.hedera.node.app.spi.key.KeyUtils.isValid;
import static java.util.Objects.requireNonNull;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.ResponseCodeEnum;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.node.app.service.evm.utils.EthSigsUtils;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.HexFormat;
/**
* A collection of static utility methods for working with aliases on {@link Account}s.
*/
public final class AliasUtils {
/**
* The first 12 bytes of an "entity num alias". See {@link #isEntityNumAlias(Bytes)}.
*
* FUTURE: The actual shard and realm are defined in config, and we should use that. However, the config can only
* be read dynamically, not statically. But, the shard and realm *cannot change* once a node has been started with
* a given state. So we really could have some static way to get the shard and realm, based on bootstrap config,
* or based on state as it has been loaded. This detail has not been worked out, and on all networks today shard
* and realm are 0, so we just let this byte array be all zeros for now.
*/
private static final byte[] ENTITY_NUM_ALIAS_PREFIX = new byte[12];
/** All EVM addresses are 20 bytes long, and key-encoded keys are not. */
private static final int EVM_ADDRESS_SIZE = 20;
/** All valid ECDSA protobuf encoded keys have this prefix */
private static final Bytes ECDSA_KEY_ALIAS_PREFIX =
Bytes.wrap(HexFormat.of().parseHex("3a21"));
/** All valid ECDSA protobuf encoded keys are 33 bytes long */
private static final int ECDSA_SECP256K1_ALIAS_SIZE = 33;
/** All valid ED25519 protobuf encoded keys are 32 bytes long */
private static final int ED25519_ALIAS_SIZE = 32;
private AliasUtils() {
throw new UnsupportedOperationException("Utility Class");
}
/**
* Gets whether the given alias is the right length to be an EVM address. Today, this number is 20 bytes.
*
* @param alias The alias to check
* @return {@code true} if the alias is the right length to be an EVM address.
*/
public static boolean isOfEvmAddressSize(@NonNull final Bytes alias) {
return alias.length() == EVM_ADDRESS_SIZE;
}
/**
* Given an alias, attempts to extract from it an EVM address. If the alias is already an EVM address, simply return
* it. If the alias is a key alias, and the key is an ECDSA_SECP256K1 key, return the EVM address derived from the
* public key. Otherwise, return null.
*
* @param alias The alias to extract an EVM address from.
* @return The EVM address, or null if the alias is not an EVM address or an ECDSA_SECP256K1 key alias.
*/
@Nullable
public static Bytes extractEvmAddress(@NonNull final Bytes alias) {
requireNonNull(alias);
if (isOfEvmAddressSize(alias)) {
return alias;
}
final var key = asKeyFromAliasOrElse(alias, null);
return (key != null && key.hasEcdsaSecp256k1()) ? recoverAddressFromPubKey(key.ecdsaSecp256k1OrThrow()) : null;
}
/**
* Given a key, attempts to extract from it an EVM address. If the key is an ECDSA_SECP256K1 key, return the EVM
* address derived from the public key. Otherwise, return null.
* @param key The key to extract an EVM address from.
* @return The EVM address, or null if the key is not an ECDSA_SECP256K1 key.
*/
@Nullable
public static Bytes extractEvmAddress(@Nullable final Key key) {
return key != null && key.hasEcdsaSecp256k1() ? recoverAddressFromPubKey(key.ecdsaSecp256k1OrThrow()) : null;
}
/**
* Given some alias, determine whether it is an "entity num alias". If the alias is exactly 20 bytes long, and
* if its initial bytes match the {@link #ENTITY_NUM_ALIAS_PREFIX}, then it is an entity num alias.
*
*
Every entity in the system (accounts, tokens, etc.) may be represented within ethereum with a 20-byte EVM
* address. This address can be explicit (as part of the alias), or it can be based on the entity ID number. When
* based on the entity number, the first 20 bytes represent the shard and alias, while the last 8 bytes represent
* the entity number. When shard and realm are zero, this prefix is all zeros, which is why it is sometimes known as
* the "long-zero" alias.
*
* @param alias The alias to check
* @return True if the alias is an entity num alias
*/
public static boolean isEntityNumAlias(final Bytes alias) {
return isOfEvmAddressSize(alias) && alias.matchesPrefix(ENTITY_NUM_ALIAS_PREFIX);
}
/**
* Given some alias, determine whether it is a key alias. If the alias is a valid protobuf-encoded key, then it is a
* key alias. This method does not check whether the key is valid, only whether the alias is a valid protobuf-encoded
* key.
* @param alias The alias to check
* @return True if the alias is a key alias
*/
public static boolean isKeyAlias(@NonNull final Bytes alias) {
final var key = asKeyFromAliasOrElse(alias, null);
if (key == null) return false;
if (!isValid(key)) return false;
if (key.hasEcdsaSecp256k1()) {
final var ecdsa = key.ecdsaSecp256k1OrThrow();
return ecdsa.length() == ECDSA_SECP256K1_ALIAS_SIZE && alias.matchesPrefix(ECDSA_KEY_ALIAS_PREFIX);
} else if (key.hasEd25519()) {
return key.ed25519OrThrow().length() == ED25519_ALIAS_SIZE;
}
return false;
}
/**
* Given a public key, recover the address from it. This method is used to extract an EVM address from an ECDSA
* public key.
* @param alias The public key to recover the address from
* @return The address recovered from the public key
*/
@NonNull
public static Bytes recoverAddressFromPubKey(@NonNull final Bytes alias) {
return EthSigsUtils.recoverAddressFromPubKey(alias);
}
/**
* Attempts to parse a {@code Key} from given alias {@code ByteString}. If the Key is of type
* Ed25519 or ECDSA(secp256k1), returns true if it is a valid key; and false otherwise.
*
* @param alias given alias byte string
* @return whether it parses to a valid primitive key
*/
public static boolean isSerializedProtoKey(@NonNull final Bytes alias) {
requireNonNull(alias);
// If the alias is an evmAddress we don't need to parse with Key.PROTOBUF.
// This will cause BufferUnderflowException
if (!isAliasSizeGreaterThanEvmAddress(alias)) {
return false;
}
// Determine whether these bytes represent a serialized Key (as protobuf bytes).
// FUTURE: Rather than parsing and catching an error, we could have PBJ provide a method that simply returns
// a boolean instead of throwing an exception. Or maybe we can make sure the alias is a valid ECDSA key length
// or ED25519 key length as a short circuit in case of very long aliases (no point parsing those).
try {
final var key = Key.PROTOBUF.parseStrict(alias.toReadableSequentialData());
return (key.hasEcdsaSecp256k1() || key.hasEd25519()) /* && isValid(key)*/;
} catch (final Exception e) {
// There are many possible exceptions thrown here, both checked (IOException) and unchecked. See the
// documentation for ReadableStreamingData as well as the parse method for all the various exceptions.
return false;
}
}
/**
* A utility method that, given an address alias, extracts the account or contract ID number (skipping shard
* and realm).
* @param addressAlias The address alias, where the 0.0.1234 style address has been encoded into 20 bytes
* @return
*/
public static Long extractIdFromAddressAlias(final Bytes addressAlias) {
return addressAlias.getLong(12);
}
/**
* A utility method that checks if account is in aliased form
* @param idOrAlias account id or alias
* @return true if account is in aliased form
*/
public static boolean isAlias(@NonNull final AccountID idOrAlias) {
requireNonNull(idOrAlias);
return !idOrAlias.hasAccountNum() && idOrAlias.hasAlias();
}
/**
* Parse a {@code Key} from given alias {@code Bytes}. If there is a parse error, throws a
* {@code HandleException} with {@code INVALID_ALIAS_KEY} response code.
* @param alias given alias bytes
* @return the parsed key
*/
@NonNull
public static Key asKeyFromAlias(@NonNull final Bytes alias) {
requireNonNull(alias);
final var key = asKeyFromAliasOrElse(alias, null);
if (key == null) throw new HandleException(ResponseCodeEnum.INVALID_ALIAS_KEY);
return key;
}
/**
* Parse a {@code Key} from given alias {@code Bytes}. If there is a parse error, throws a
* {@code HandleException} with {@code INVALID_ALIAS_KEY} response code.
* @param alias given alias bytes
* @return the parsed key
* @throws PreCheckException if the alias is not a valid key
*/
@NonNull
public static Key asKeyFromAliasPreCheck(@NonNull final Bytes alias) throws PreCheckException {
requireNonNull(alias);
final var key = asKeyFromAliasOrElse(alias, null);
if (key == null) throw new PreCheckException(ResponseCodeEnum.INVALID_ALIAS_KEY);
return key;
}
/**
* Parse a {@code Key} from given alias {@code Bytes}. If there is a parse error, returns the given default key.
* @param alias given alias bytes. If the alias is an evmAddress we don't need to parse with Key.PROTOBUF.
* This will cause BufferUnderflowException. So, we return the default key.
* @param def default key
* @return the parsed key or the default key if there is a parse error
*/
@Nullable
public static Key asKeyFromAliasOrElse(@NonNull final Bytes alias, @Nullable final Key def) {
requireNonNull(alias);
// If the alias is an evmAddress we don't need to parse with Key.PROTOBUF.
// This will cause BufferUnderflowException
if (!isAliasSizeGreaterThanEvmAddress(alias)) {
return def;
}
try {
return Key.PROTOBUF.parseStrict(alias.toReadableSequentialData());
} catch (final Exception e) {
// There are many possible exceptions, not just IOException. We want to catch all of them.
return def;
}
}
/**
* Check if the given alias is greater than the size of an EVM address.
* @param alias The alias to check
* @return True if the alias is greater than the size of an EVM address
*/
public static boolean isAliasSizeGreaterThanEvmAddress(@NonNull final Bytes alias) {
requireNonNull(alias);
return alias.length() > EVM_ADDRESS_SIZE;
}
}