code/pallets/subtensor/src/utils/evm.rs

evm.rs

244 lines · 9,494 bytes · 19a6485969RawGitHub
use super::*;
use alloc::string::ToString;
use frame_support::ensure;
use frame_system::ensure_signed;

use sp_core::{H160, ecdsa::Signature, hashing::keccak_256};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::vec::Vec;
use subtensor_runtime_common::NetUid;

const MESSAGE_PREFIX: &str = "\x19Ethereum Signed Message:\n";

impl<T: Config> Pallet<T> {
    pub(crate) fn hash_message_eip191<M: AsRef<[u8]>>(message: M) -> [u8; 32] {
        let msg_len = message.as_ref().len().to_string();
        keccak_256(
            &[
                MESSAGE_PREFIX.as_bytes(),
                msg_len.as_bytes(),
                message.as_ref(),
            ]
            .concat(),
        )
    }

    /// Associate an EVM key with a hotkey.
    ///
    /// This function accepts a Signature, which is a signed message containing the hotkey
    /// concatenated with the hashed block number. It will then attempt to recover the EVM key from
    /// the signature and compare it with the `evm_key` parameter, and ensures that they match.
    ///
    /// The EVM key is expected to sign the message according to this formula to produce the
    /// signature:
    /// ```text
    /// keccak_256(hotkey ++ keccak_256(block_number))
    /// ```
    ///
    /// # Arguments
    ///
    /// * `origin`: The origin of the call, which should be the hotkey.
    /// * `netuid`: The unique identifier for the subnet that the hotkey belongs to.
    /// * `evm_key`: The EVM address to associate with the `hotkey`.
    /// * `block_number`: The block number used in the `signature`.
    /// * `signature`: A signed message by the `evm_key` containing the `hotkey` and the hashed
    ///   `block_number`.
    pub fn do_associate_evm_key(
        origin: OriginFor<T>,
        netuid: NetUid,
        evm_key: H160,
        block_number: u64,
        mut signature: Signature,
    ) -> dispatch::DispatchResult {
        let hotkey = ensure_signed(origin)?;
        ensure!(Self::if_subnet_exist(netuid), Error::<T>::SubnetNotExists);
        ensure!(
            Self::get_owning_coldkey_for_hotkey(&hotkey) != DefaultAccount::<T>::get(),
            Error::<T>::NonAssociatedColdKey
        );

        // Normalize the v value to 0 or 1
        if signature.0[64] >= 27 {
            signature.0[64] = signature.0[64].saturating_sub(27);
        }

        let uid = Self::get_uid_for_net_and_hotkey(netuid, &hotkey)?;
        Self::ensure_evm_key_associate_rate_limit(netuid, uid)?;

        let block_hash = keccak_256(block_number.encode().as_ref());
        let message = [
            hotkey.encode().as_ref(),
            <[u8; 32] as AsRef<[u8]>>::as_ref(&block_hash),
        ]
        .concat();
        let public = signature
            .recover_prehashed(&Self::hash_message_eip191(message))
            .ok_or(Error::<T>::InvalidIdentity)?;
        let secp_pubkey = libsecp256k1::PublicKey::parse_compressed(&public.0)
            .map_err(|_| Error::<T>::UnableToRecoverPublicKey)?;
        let uncompressed = secp_pubkey.serialize();
        let hashed_evm_key = H160::from_slice(&keccak_256(&uncompressed[1..])[12..]);

        ensure!(
            evm_key == hashed_evm_key,
            Error::<T>::InvalidRecoveredPublicKey
        );

        Self::ensure_evm_address_index_capacity(netuid, uid, evm_key)?;

        let current_block_number = Self::get_current_block_as_u64();

        Self::set_associated_evm_address(netuid, uid, evm_key, current_block_number);

        Self::deposit_event(Event::EvmKeyAssociated {
            netuid,
            hotkey,
            evm_key,
            block_associated: current_block_number,
        });

        Ok(())
    }

    /// Ensure `evm_key` can accept an association for `uid` on `netuid` without exceeding
    /// [`MAX_ASSOCIATED_UIDS_PER_EVM_ADDRESS`].
    ///
    /// Re-associating a UID that is already tracked for this address (e.g. refreshing its
    /// block) never consumes a new slot, so it is always permitted. This is the only path
    /// that grows a bucket, so enforcing the cap here keeps the forward map
    /// (`AssociatedEvmAddress`) and reverse index (`AssociatedUidsByEvmAddress`) consistent.
    pub fn ensure_evm_address_index_capacity(
        netuid: NetUid,
        uid: u16,
        evm_key: H160,
    ) -> DispatchResult {
        let bucket = AssociatedUidsByEvmAddress::<T>::get(netuid, evm_key);
        let already_tracked = bucket.iter().any(|(stored_uid, _)| *stored_uid == uid);
        ensure!(
            already_tracked || (bucket.len() as u32) < crate::MAX_ASSOCIATED_UIDS_PER_EVM_ADDRESS,
            Error::<T>::EvmKeyAssociationLimitExceeded
        );
        Ok(())
    }

    pub fn set_associated_evm_address(
        netuid: NetUid,
        uid: u16,
        evm_key: H160,
        block_associated: u64,
    ) {
        if let Some((old_evm_key, _)) = AssociatedEvmAddress::<T>::get(netuid, uid)
            && old_evm_key != evm_key
        {
            Self::remove_uid_from_evm_address_index(netuid, old_evm_key, uid);
        }

        Self::upsert_uid_in_evm_address_index(netuid, evm_key, uid, block_associated);
        AssociatedEvmAddress::<T>::insert(netuid, uid, (evm_key, block_associated));
    }

    pub fn remove_associated_evm_address(netuid: NetUid, uid: u16) {
        if let Some((evm_key, _)) = AssociatedEvmAddress::<T>::take(netuid, uid) {
            Self::remove_uid_from_evm_address_index(netuid, evm_key, uid);
        }
    }

    pub fn clear_associated_evm_addresses(netuid: NetUid) {
        let _ = AssociatedEvmAddress::<T>::clear_prefix(netuid, u32::MAX, None);
        let _ = AssociatedUidsByEvmAddress::<T>::clear_prefix(netuid, u32::MAX, None);
    }

    /// Remap the UIDs stored in the EVM-address reverse index after a subnet UID compaction.
    ///
    /// [`Self::trim_to_max_allowed_uids`] compacts kept UIDs into new, lower positions described
    /// by `old_to_new_uid`; trimmed UIDs have already been dropped from both the forward map and
    /// this reverse index. Only the UID *positions* of kept associations change (their EVM key
    /// and block are untouched), so we rewrite each bucket's UIDs through the mapping rather than
    /// clearing the whole index and rebuilding it from the forward map.
    ///
    /// Work is bounded by the number of distinct EVM addresses associated on the subnet: each
    /// reverse-index entry is read once and written at most once, and the forward map is never
    /// re-scanned.
    pub fn remap_associated_evm_address_index(
        netuid: NetUid,
        old_to_new_uid: &BTreeMap<usize, usize>,
    ) {
        // Collect first: rewriting entries while iterating the same storage prefix is unsafe.
        let updates: Vec<_> = AssociatedUidsByEvmAddress::<T>::iter_prefix(netuid)
            .filter_map(|(evm_key, mut bucket)| {
                let mut changed = false;
                for (uid, _) in bucket.iter_mut() {
                    if let Some(&new_uid) = old_to_new_uid.get(&(*uid as usize))
                        && *uid != new_uid as u16
                    {
                        *uid = new_uid as u16;
                        changed = true;
                    }
                }
                changed.then_some((evm_key, bucket))
            })
            .collect();

        for (evm_key, bucket) in updates {
            AssociatedUidsByEvmAddress::<T>::insert(netuid, evm_key, bucket);
        }
    }

    fn upsert_uid_in_evm_address_index(
        netuid: NetUid,
        evm_key: H160,
        uid: u16,
        block_associated: u64,
    ) {
        AssociatedUidsByEvmAddress::<T>::mutate(netuid, evm_key, |uids| {
            if let Some((_, stored_block)) =
                uids.iter_mut().find(|(stored_uid, _)| *stored_uid == uid)
            {
                *stored_block = block_associated;
                return;
            }

            if uids.try_push((uid, block_associated)).is_err() {
                log::error!(
                    "AssociatedUidsByEvmAddress overflow for netuid {netuid:?}, evm_key {evm_key:?}"
                );
            }
        });
    }

    fn remove_uid_from_evm_address_index(netuid: NetUid, evm_key: H160, uid: u16) {
        AssociatedUidsByEvmAddress::<T>::mutate_exists(netuid, evm_key, |maybe_uids| {
            let Some(uids) = maybe_uids else {
                return;
            };

            uids.retain(|(stored_uid, _)| *stored_uid != uid);

            if uids.is_empty() {
                *maybe_uids = None;
            }
        });
    }

    pub fn uid_lookup(netuid: NetUid, evm_key: H160, limit: u16) -> Vec<(u16, u64)> {
        let mut ret_val = AssociatedUidsByEvmAddress::<T>::get(netuid, evm_key)
            .into_iter()
            .collect::<Vec<(u16, u64)>>();
        ret_val.sort_by(|(_, block1), (_, block2)| block1.cmp(block2));
        ret_val.truncate(limit as usize);
        ret_val
    }

    pub fn ensure_evm_key_associate_rate_limit(netuid: NetUid, uid: u16) -> DispatchResult {
        let now = Self::get_current_block_as_u64();
        let block_associated = match AssociatedEvmAddress::<T>::get(netuid, uid) {
            Some((_, block_associated)) => block_associated,
            None => 0,
        };
        let block_diff = now.saturating_sub(block_associated);
        if block_diff < T::EvmKeyAssociateRateLimit::get() {
            return Err(Error::<T>::EvmKeyAssociateRateLimitExceeded.into());
        }
        Ok(())
    }
}