diff --git a/Cargo.lock b/Cargo.lock index 51a10fdf0..7e5d44fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4247,7 +4247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -4718,12 +4718,12 @@ dependencies = [ [[package]] name = "libsecp256k1" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" dependencies = [ "arrayref", - "base64 0.13.1", + "base64 0.22.1", "digest 0.9.0", "hmac-drbg", "libsecp256k1-core", @@ -6456,6 +6456,7 @@ dependencies = [ "frame-system", "hex", "hex-literal", + "libsecp256k1", "log", "ndarray", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 15dd760a5..b1456c6ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ futures = "0.3.30" hex = { version = "0.4", default-features = false } hex-literal = "0.4.1" jsonrpsee = { version = "0.24.4", default-features = false } +libsecp256k1 = { version = "0.7.2", default-features = false } log = { version = "0.4.21", default-features = false } memmap2 = "0.9.4" ndarray = { version = "0.15.6", default-features = false } diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index f240245c4..ab17cf5bd 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -34,6 +34,7 @@ serde_bytes = { workspace = true, features = ["alloc"] } serde_with = { workspace = true, features = ["macros"] } sp-runtime = { workspace = true } sp-std = { workspace = true } +libsecp256k1 = { workspace = true } log = { workspace = true } substrate-fixed = { workspace = true } pallet-transaction-payment = { workspace = true } @@ -91,6 +92,7 @@ std = [ "sp-tracing/std", "sp-version/std", "hex/std", + "libsecp256k1/std", "log/std", "ndarray/std", "serde/std", diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 0024b1587..e360c307e 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -77,7 +77,7 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; use pallet_drand::types::RoundNumber; - use sp_core::{ConstU32, H256}; + use sp_core::{ConstU32, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::vec_deque::VecDeque; use sp_std::vec; @@ -1561,6 +1561,14 @@ pub mod pallet { OptionQuery, >; + /// ============================= + /// ==== EVM related storage ==== + /// ============================= + #[pallet::storage] + /// --- DMAP (netuid, uid) --> (H160, last_block_where_ownership_was_proven) + pub type AssociatedEvmAddress = + StorageDoubleMap<_, Twox64Concat, u16, Twox64Concat, u16, (H160, u64), OptionQuery>; + /// ================== /// ==== Genesis ===== /// ================== diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 9158073e1..4ea03c957 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -9,6 +9,7 @@ mod dispatches { use frame_support::traits::schedule::DispatchTime; use frame_support::traits::schedule::v3::Anon as ScheduleAnon; use frame_system::pallet_prelude::BlockNumberFor; + use sp_core::ecdsa::Signature; use sp_runtime::traits::Saturating; use crate::MAX_CRV3_COMMIT_SIZE_BYTES; @@ -1929,6 +1930,50 @@ mod dispatches { Ok(()) } + /// Attempts to associate a hotkey with an EVM key. + /// + /// The signature will be checked to see if the recovered public key matches the `evm_key` provided. + /// + /// 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 transaction, which must be signed by the coldkey that owns the `hotkey`. + /// * `netuid` - The netuid that the `hotkey` belongs to. + /// * `hotkey` - The hotkey associated with the `origin`. + /// * `evm_key` - The EVM key 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`. + /// + /// # Errors + /// Returns an error if: + /// * The transaction is not signed. + /// * The hotkey is not owned by the origin coldkey. + /// * The hotkey does not belong to the subnet identified by the netuid. + /// * The EVM key cannot be recovered from the signature. + /// * The EVM key recovered from the signature does not match the given EVM key. + /// + /// # Events + /// May emit a `EvmKeyAssociated` event on success + #[pallet::call_index(93)] + #[pallet::weight(( + Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().reads_writes(2, 1)), + DispatchClass::Operational, + Pays::Yes + ))] + pub fn associate_evm_key( + origin: T::RuntimeOrigin, + netuid: u16, + hotkey: T::AccountId, + evm_key: H160, + block_number: u64, + signature: Signature, + ) -> DispatchResult { + Self::do_associate_evm_key(origin, netuid, hotkey, evm_key, block_number, signature) + } + /// Recycles alpha from a cold/hot key pair, reducing AlphaOut on a subnet /// /// # Arguments diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 3404b36d8..052b15d5e 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -203,5 +203,9 @@ mod errors { NotEnoughAlphaOutToRecycle, /// Cannot burn or recycle TAO from root subnet CannotBurnOrRecycleOnRootSubnet, + /// Public key cannot be recovered. + UnableToRecoverPublicKey, + /// Recovered public key is invalid. + InvalidRecoveredPublicKey, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 6f3e02dc3..8c2e863d0 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -294,6 +294,18 @@ mod events { /// (coldkey, hotkey, amount, subnet_id) AlphaBurned(T::AccountId, T::AccountId, u64, u16), + /// An EVM key has been associated with a hotkey. + EvmKeyAssociated { + /// The subnet that the hotkey belongs to. + netuid: u16, + /// The hotkey associated with the EVM key. + hotkey: T::AccountId, + /// The EVM key being associated with the hotkey. + evm_key: H160, + /// The block where the association happened. + block_associated: u64, + }, + /// CRV3 Weights have been successfully revealed. /// /// - **netuid**: The network identifier. diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index b902cb664..842e9d0b9 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -134,6 +134,21 @@ impl Pallet { // Ensure that the hotkey has enough stake to withdraw. let alpha_unstaked = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + if Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + alpha_unstaked, + false, + ) + .is_err() + { + // Don't unstake from this netuid + continue; + } + let fee = Self::calculate_staking_fee( Some((&hotkey, netuid)), &coldkey, @@ -211,6 +226,21 @@ impl Pallet { // Ensure that the hotkey has enough stake to withdraw. let alpha_unstaked = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + if Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + alpha_unstaked, + false, + ) + .is_err() + { + // Don't unstake from this netuid + continue; + } + let fee = Self::calculate_staking_fee( Some((&hotkey, netuid)), &coldkey, diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index ae07ad760..d00c47844 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -58,9 +58,15 @@ impl Pallet { } } pub fn update_moving_price(netuid: u16) { - let blocks_since_registration = U96F32::saturating_from_num( - Self::get_current_block_as_u64().saturating_sub(NetworkRegisteredAt::::get(netuid)), - ); + let blocks_since_start_call = U96F32::saturating_from_num({ + // We expect FirstEmissionBlockNumber to be set earlier, and we take the block when + // `start_call` was called (first block before FirstEmissionBlockNumber). + let start_call_block = FirstEmissionBlockNumber::::get(netuid) + .unwrap_or_default() + .saturating_sub(1); + + Self::get_current_block_as_u64().saturating_sub(start_call_block) + }); // Use halving time hyperparameter. The meaning of this parameter can be best explained under // the assumption of a constant price and SubnetMovingAlpha == 0.5: It is how many blocks it @@ -68,8 +74,8 @@ impl Pallet { // by half. let halving_time = EMAPriceHalvingBlocks::::get(netuid); let current_ma_unsigned = U96F32::saturating_from_num(SubnetMovingAlpha::::get()); - let alpha: U96F32 = current_ma_unsigned.saturating_mul(blocks_since_registration.safe_div( - blocks_since_registration.saturating_add(U96F32::saturating_from_num(halving_time)), + let alpha: U96F32 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( + blocks_since_start_call.saturating_add(U96F32::saturating_from_num(halving_time)), )); // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. // We can use unsigned type here: U96F32 diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index eaa199755..1345f36b7 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -190,7 +190,7 @@ fn test_coinbase_moving_prices() { SubnetAlphaIn::::insert(netuid, 1_000_000); SubnetMechanism::::insert(netuid, 1); SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); - NetworkRegisteredAt::::insert(netuid, 1); + FirstEmissionBlockNumber::::insert(netuid, 1); // Updating the moving price keeps it the same. assert_eq!( @@ -250,7 +250,7 @@ fn test_update_moving_price_initial() { // Registered recently System::set_block_number(510); - NetworkRegisteredAt::::insert(netuid, 500); + FirstEmissionBlockNumber::::insert(netuid, 500); SubtensorModule::update_moving_price(netuid); @@ -275,7 +275,7 @@ fn test_update_moving_price_after_time() { // Registered long time ago System::set_block_number(144_000_500); - NetworkRegisteredAt::::insert(netuid, 500); + FirstEmissionBlockNumber::::insert(netuid, 500); SubtensorModule::update_moving_price(netuid); diff --git a/pallets/subtensor/src/tests/evm.rs b/pallets/subtensor/src/tests/evm.rs new file mode 100644 index 000000000..bdd55c196 --- /dev/null +++ b/pallets/subtensor/src/tests/evm.rs @@ -0,0 +1,246 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::indexing_slicing +)] + +use super::mock::*; +use crate::*; +use frame_support::testing_prelude::*; +use sp_core::{H160, Pair, U256, blake2_256, ecdsa, keccak_256}; + +fn public_to_evm_key(pubkey: &ecdsa::Public) -> H160 { + use libsecp256k1::PublicKey; + use sp_core::keccak_256; + + let secp_pub = PublicKey::parse_compressed(&pubkey.0).expect("Invalid pubkey"); + let uncompressed = secp_pub.serialize(); // 65 bytes: 0x04 + X + Y + let hash = keccak_256(&uncompressed[1..]); // drop 0x04 + let mut address = [0u8; 20]; + address.copy_from_slice(&hash[12..]); + H160::from(address) +} + +#[test] +fn test_associate_evm_key_success() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + + let tempo: u16 = 2; + let modality: u16 = 2; + + add_network(netuid, tempo, modality); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + register_ok_neuron(netuid, hotkey, coldkey, 0); + + let pair = ecdsa::Pair::generate().0; + let public = pair.public(); + let evm_key = public_to_evm_key(&public); + let block_number = frame_system::Pallet::::block_number(); + let hashed_block_number = keccak_256(block_number.encode().as_ref()); + let hotkey_bytes = hotkey.encode(); + + let mut message = [0u8; 64]; + message[..32].copy_from_slice(hotkey_bytes.as_ref()); + message[32..].copy_from_slice(hashed_block_number.as_ref()); + let hashed_message = keccak_256(message.as_ref()); + let signature = pair.sign_prehashed(&hashed_message); + + assert_ok!(SubtensorModule::associate_evm_key( + RuntimeOrigin::signed(coldkey), + netuid, + hotkey, + evm_key, + block_number, + signature, + )); + + System::assert_last_event( + Event::EvmKeyAssociated { + netuid, + hotkey, + evm_key, + block_associated: block_number, + } + .into(), + ); + }); +} + +#[test] +fn test_associate_evm_key_different_block_number_success() { + new_test_ext(100).execute_with(|| { + let netuid: u16 = 1; + + let tempo: u16 = 2; + let modality: u16 = 2; + + add_network(netuid, tempo, modality); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + register_ok_neuron(netuid, hotkey, coldkey, 0); + + let pair = ecdsa::Pair::generate().0; + let public = pair.public(); + let evm_key = public_to_evm_key(&public); + let block_number = 99u64; + let hashed_block_number = keccak_256(block_number.encode().as_ref()); + let hotkey_bytes = hotkey.encode(); + + let mut message = [0u8; 64]; + message[..32].copy_from_slice(hotkey_bytes.as_ref()); + message[32..].copy_from_slice(hashed_block_number.as_ref()); + let hashed_message = keccak_256(message.as_ref()); + let signature = pair.sign_prehashed(&hashed_message); + + assert_ok!(SubtensorModule::associate_evm_key( + RuntimeOrigin::signed(coldkey), + netuid, + hotkey, + evm_key, + block_number, + signature, + )); + + System::assert_last_event( + Event::EvmKeyAssociated { + netuid, + hotkey, + evm_key, + block_associated: frame_system::Pallet::::block_number(), + } + .into(), + ); + }); +} + +#[test] +fn test_associate_evm_key_coldkey_does_not_own_hotkey() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + + let tempo: u16 = 2; + let modality: u16 = 2; + + add_network(netuid, tempo, modality); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let pair = ecdsa::Pair::generate().0; + let public = pair.public(); + let evm_key = public_to_evm_key(&public); + let block_number = frame_system::Pallet::::block_number(); + let hashed_block_number = keccak_256(block_number.encode().as_ref()); + let hotkey_bytes = hotkey.encode(); + + let mut message = [0u8; 64]; + message[..32].copy_from_slice(hotkey_bytes.as_ref()); + message[32..].copy_from_slice(hashed_block_number.as_ref()); + let hashed_message = keccak_256(message.as_ref()); + let signature = pair.sign_prehashed(&hashed_message); + + assert_err!( + SubtensorModule::associate_evm_key( + RuntimeOrigin::signed(coldkey), + netuid, + hotkey, + evm_key, + block_number, + signature, + ), + Error::::NonAssociatedColdKey + ); + }); +} + +#[test] +fn test_associate_evm_key_hotkey_not_registered_in_subnet() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + + let tempo: u16 = 2; + let modality: u16 = 2; + + add_network(netuid, tempo, modality); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + let pair = ecdsa::Pair::generate().0; + let public = pair.public(); + let evm_key = public_to_evm_key(&public); + let block_number = frame_system::Pallet::::block_number(); + let hashed_block_number = keccak_256(block_number.encode().as_ref()); + let hotkey_bytes = hotkey.encode(); + + let mut message = [0u8; 64]; + message[..32].copy_from_slice(hotkey_bytes.as_ref()); + message[32..].copy_from_slice(hashed_block_number.as_ref()); + let hashed_message = keccak_256(message.as_ref()); + let signature = pair.sign_prehashed(&hashed_message); + + assert_err!( + SubtensorModule::associate_evm_key( + RuntimeOrigin::signed(coldkey), + netuid, + hotkey, + evm_key, + block_number, + signature, + ), + Error::::HotKeyNotRegisteredInSubNet + ); + }); +} + +#[test] +fn test_associate_evm_key_using_wrong_hash_function() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + + let tempo: u16 = 2; + let modality: u16 = 2; + + add_network(netuid, tempo, modality); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + register_ok_neuron(netuid, hotkey, coldkey, 0); + + let pair = ecdsa::Pair::generate().0; + let public = pair.public(); + let evm_key = public_to_evm_key(&public); + let block_number = frame_system::Pallet::::block_number(); + let hashed_block_number = keccak_256(block_number.encode().as_ref()); + let hotkey_bytes = hotkey.encode(); + + let mut message = [0u8; 64]; + message[..32].copy_from_slice(hotkey_bytes.as_ref()); + message[32..].copy_from_slice(hashed_block_number.as_ref()); + let hashed_message = blake2_256(message.as_ref()); + let signature = pair.sign_prehashed(&hashed_message); + + assert_err!( + SubtensorModule::associate_evm_key( + RuntimeOrigin::signed(coldkey), + netuid, + hotkey, + evm_key, + block_number, + signature, + ), + Error::::InvalidRecoveredPublicKey + ); + }); +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index efd45ddef..ce891e561 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -5,6 +5,7 @@ mod delegate_info; mod difficulty; mod emission; mod epoch; +mod evm; mod math; mod migration; mod mock; diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index fe6113548..a9fa11ba3 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -3,6 +3,8 @@ use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::RawOrigin; +use safe_math::SafeDiv; +use substrate_fixed::traits::FromFixed; use super::mock::*; use crate::*; @@ -10,7 +12,7 @@ use approx::assert_abs_diff_eq; use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays}; use frame_support::sp_runtime::DispatchError; use sp_core::{Get, H256, U256}; -use substrate_fixed::types::{I96F32, U64F64, U96F32}; +use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; /*********************************************************** staking::add_stake() tests @@ -4240,3 +4242,192 @@ fn test_move_stake_limit_partial() { assert_abs_diff_eq!(new_alpha, 149_000_000_000, epsilon = 100_000_000,); }); } + +#[test] +fn test_unstake_all_hits_liquidity_min() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = 190_000_000_000; // 190 Alpha + + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will bring liqudity below the minimum + let remaining_tao: I96F32 = + DefaultMinimumPoolLiquidity::::get().saturating_sub(I96F32::from(1)); + let alpha_reserves: I110F18 = I110F18::from(stake_amount + 10_000_000); + let alpha = stake_amount; + + let k: I110F18 = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(alpha))); + let tao_reserves: I110F18 = k.safe_div(alpha_reserves); + + SubnetTAO::::insert(netuid, tao_reserves.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_reserves.to_num::()); + + // Try to unstake, but we reduce liquidity too far + + assert_ok!(SubtensorModule::unstake_all( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Expect nothing to be unstaked + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, stake_amount, epsilon = 0,); + }); +} + +#[test] +fn test_unstake_all_alpha_hits_liquidity_min() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = 190_000_000_000; // 190 Alpha + + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will bring liqudity below the minimum + let remaining_tao: I96F32 = + DefaultMinimumPoolLiquidity::::get().saturating_sub(I96F32::from(1)); + let alpha_reserves: I110F18 = I110F18::from(stake_amount + 10_000_000); + let alpha = stake_amount; + + let k: I110F18 = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(alpha))); + let tao_reserves: I110F18 = k.safe_div(alpha_reserves); + + SubnetTAO::::insert(netuid, tao_reserves.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_reserves.to_num::()); + + // Try to unstake, but we reduce liquidity too far + + assert_ok!(SubtensorModule::unstake_all_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Expect nothing to be unstaked + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, stake_amount, epsilon = 0,); + }); +} + +#[test] +fn test_unstake_all_alpha_works() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = 190_000_000_000; // 190 Alpha + + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will keep liq above min + let remaining_tao: I96F32 = + DefaultMinimumPoolLiquidity::::get().saturating_add(I96F32::from(10_000_000)); + let alpha_reserves: I110F18 = I110F18::from(stake_amount + 10_000_000); + let alpha = stake_amount; + + let k: I110F18 = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(alpha))); + let tao_reserves: I110F18 = k.safe_div(alpha_reserves); + + SubnetTAO::::insert(netuid, tao_reserves.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_reserves.to_num::()); + + // Unstake all alpha to root + assert_ok!(SubtensorModule::unstake_all_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, 0, epsilon = 1_000,); + let new_root = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, 0); + assert!(new_root > 100_000); + }); +} + +#[test] +fn test_unstake_all_works() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = 190_000_000_000; // 190 Alpha + + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will keep liq above min + let remaining_tao: I96F32 = + DefaultMinimumPoolLiquidity::::get().saturating_add(I96F32::from(10_000_000)); + let alpha_reserves: I110F18 = I110F18::from(stake_amount + 10_000_000); + let alpha = stake_amount; + + let k: I110F18 = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(alpha))); + let tao_reserves: I110F18 = k.safe_div(alpha_reserves); + + SubnetTAO::::insert(netuid, tao_reserves.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_reserves.to_num::()); + + // Unstake all alpha to root + assert_ok!(SubtensorModule::unstake_all( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, 0, epsilon = 1_000,); + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey); + assert!(new_balance > 100_000); + }); +} diff --git a/pallets/subtensor/src/utils/evm.rs b/pallets/subtensor/src/utils/evm.rs new file mode 100644 index 000000000..a34f6afc8 --- /dev/null +++ b/pallets/subtensor/src/utils/evm.rs @@ -0,0 +1,74 @@ +use super::*; + +use frame_support::ensure; +use frame_system::ensure_signed; +use sp_core::{H160, ecdsa::Signature, hashing::keccak_256}; + +impl Pallet { + /// 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 coldkey that owns the hotkey. + /// * `netuid` - The unique identifier for the subnet that the hotkey belongs to. + /// * `hotkey` - The hotkey associated with the `origin` coldkey. + /// * `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: T::RuntimeOrigin, + netuid: u16, + hotkey: T::AccountId, + evm_key: H160, + block_number: u64, + signature: Signature, + ) -> dispatch::DispatchResult { + let coldkey = ensure_signed(origin)?; + + ensure!( + Self::get_owning_coldkey_for_hotkey(&hotkey) == coldkey, + Error::::NonAssociatedColdKey + ); + + let uid = Self::get_uid_for_net_and_hotkey(netuid, &hotkey)?; + + let mut message = [0u8; 64]; + let block_hash = keccak_256(block_number.encode().as_ref()); + message[..32].copy_from_slice(&hotkey.encode()[..]); + message[32..].copy_from_slice(block_hash.as_ref()); + let public = signature + .recover_prehashed(&keccak_256(message.as_ref())) + .ok_or(Error::::UnableToRecoverPublicKey)?; + let secp_pubkey = libsecp256k1::PublicKey::parse_compressed(&public.0) + .map_err(|_| Error::::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::::InvalidRecoveredPublicKey + ); + + let current_block_number = Self::get_current_block_as_u64(); + + AssociatedEvmAddress::::insert(netuid, uid, (evm_key, current_block_number)); + + Self::deposit_event(Event::EvmKeyAssociated { + netuid, + hotkey, + evm_key, + block_associated: current_block_number, + }); + + Ok(()) + } +} diff --git a/pallets/subtensor/src/utils/mod.rs b/pallets/subtensor/src/utils/mod.rs index 909ad8959..3eb843995 100644 --- a/pallets/subtensor/src/utils/mod.rs +++ b/pallets/subtensor/src/utils/mod.rs @@ -1,4 +1,5 @@ use super::*; +pub mod evm; pub mod identity; pub mod misc; pub mod rate_limiting; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 48a509e22..5b87dbf03 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -207,7 +207,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 259, + spec_version: 260, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,