Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ mod dispatches {
/// * 'MaxWeightExceeded':
/// - Attempting to set weights with max value exceeding limit.
#[pallet::call_index(0)]
#[pallet::weight((<T as crate::pallet::Config>::WeightInfo::set_weights(), DispatchClass::Normal, Pays::No))]
#[pallet::weight((<T as crate::pallet::Config>::WeightInfo::set_weights(), DispatchClass::Normal, Pays::Yes))]
pub fn set_weights(
origin: OriginFor<T>,
netuid: NetUid,
Expand Down
4 changes: 4 additions & 0 deletions pallets/subtensor/src/rpc_info/delegate_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,8 @@ impl<T: Config> Pallet<T> {
pub fn get_coldkey_for_hotkey(hotkey: &T::AccountId) -> T::AccountId {
Owner::<T>::get(hotkey)
}

pub fn maybe_coldkey_for_hotkey(hotkey: &T::AccountId) -> Option<T::AccountId> {
Owner::<T>::try_get(hotkey).ok()
}
Comment on lines +191 to +194
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is super weird that there is a DefaultAccount for a Owner, it should probably be an OptionQuery but that is something for another discussion

}
2 changes: 1 addition & 1 deletion pallets/subtensor/src/tests/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ fn test_set_weights_dispatch_info_ok() {
let dispatch_info = call.get_dispatch_info();

assert_eq!(dispatch_info.class, DispatchClass::Normal);
assert_eq!(dispatch_info.pays_fee, Pays::No);
assert_eq!(dispatch_info.pays_fee, Pays::Yes);
});
}

Expand Down
31 changes: 27 additions & 4 deletions runtime/src/transaction_payment_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ where
}
}

impl<T: Config + pallet_proxy::Config + pallet_utility::Config> ChargeTransactionPaymentWrapper<T>
impl<T: Config + pallet_proxy::Config + pallet_utility::Config + pallet_subtensor::Config>
ChargeTransactionPaymentWrapper<T>
where
RuntimeCallOf<T>: IsSubType<pallet_proxy::Call<T>> + IsSubType<pallet_utility::Call<T>>,
RuntimeCallOf<T>: IsSubType<pallet_proxy::Call<T>>
+ IsSubType<pallet_utility::Call<T>>
+ IsSubType<pallet_subtensor::Call<T>>,
RuntimeOriginOf<T>: AsSystemOriginSigner<AccountIdOf<T>> + Clone,
{
/// Extract (real, delegate, inner_call) from a `proxy` call.
Expand Down Expand Up @@ -182,14 +185,28 @@ where

common_real
}

fn extract_coldkey_fee_payer(origin: &RuntimeOriginOf<T>) -> Option<AccountIdOf<T>> {
let signer = origin.as_system_origin_signer()?;

pallet_subtensor::Pallet::<T>::maybe_coldkey_for_hotkey(signer)
}

fn is_coldkey_fee_payer_eligible(call: &RuntimeCallOf<T>) -> bool {
matches!(
call.is_sub_type(),
Some(pallet_subtensor::Call::set_weights { .. })
)
}
}

impl<T: Config + pallet_proxy::Config + pallet_utility::Config>
impl<T: Config + pallet_proxy::Config + pallet_utility::Config + pallet_subtensor::Config>
TransactionExtension<RuntimeCallOf<T>> for ChargeTransactionPaymentWrapper<T>
where
RuntimeCallOf<T>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+ IsSubType<pallet_proxy::Call<T>>
+ IsSubType<pallet_utility::Call<T>>,
+ IsSubType<pallet_utility::Call<T>>
+ IsSubType<pallet_subtensor::Call<T>>,
RuntimeOriginOf<T>: AsSystemOriginSigner<AccountIdOf<T>>
+ Clone
+ From<frame_system::RawOrigin<AccountIdOf<T>>>,
Expand Down Expand Up @@ -230,6 +247,12 @@ where
// Otherwise, the signer pays as usual.
let fee_origin = if let Some(real) = Self::extract_real_fee_payer(call, &origin) {
frame_system::RawOrigin::Signed(real).into()
} else if Self::is_coldkey_fee_payer_eligible(call) {
if let Some(coldkey) = Self::extract_coldkey_fee_payer(&origin) {
frame_system::RawOrigin::Signed(coldkey).into()
} else {
origin.clone()
}
Comment on lines +250 to +255
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't just have extract_coldkey_fee_payer returns None when not eligible directly in the same function to collapse both into one?

Make things cleaner, wdyt?

Suggested change
} else if Self::is_coldkey_fee_payer_eligible(call) {
if let Some(coldkey) = Self::extract_coldkey_fee_payer(&origin) {
frame_system::RawOrigin::Signed(coldkey).into()
} else {
origin.clone()
}
} else if let Some(coldkey) Self::extract_coldkey_fee_payer(call, &origin) {
frame_system::RawOrigin::Signed(coldkey).into()

} else {
origin.clone()
};
Expand Down
139 changes: 139 additions & 0 deletions runtime/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)]

use {
Comment on lines +1 to +3
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is subtensor pallet specific if we think about it. Maybe exposing the mock from the subtensor pallet make it easier?

frame_support::assert_ok,
node_subtensor_runtime::ExistentialDeposit,
node_subtensor_runtime::{BuildStorage, Runtime, RuntimeGenesisConfig, System},
pallet_subtensor::{
BurnHalfLife, BurnIncreaseMult, Error, FirstEmissionBlockNumber, Pallet as SubtensorPallet,
SubnetAlphaIn, SubnetAlphaInProvided, SubnetTAO, SubtokenEnabled,
},
substrate_fixed::types::U64F64,
subtensor_runtime_common::{AccountId, AlphaBalance, NetUid, TaoBalance},
};

pub const ONE: [u8; 32] = [1_u8; 32];
pub const TWO: [u8; 32] = [2_u8; 32];
pub const THREE: [u8; 32] = [3_u8; 32];
pub const FOUR_NO_BALANCE: [u8; 32] = [4_u8; 32];

pub fn new_test_ext() -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let amount = TaoBalance::from(1_000_000_000_000_u64);
let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig {
balances: pallet_balances::GenesisConfig {
balances: vec![
(AccountId::from(ONE), amount),
(AccountId::from(TWO), amount),
(AccountId::from(THREE), amount),
],
dev_accounts: None,
},
..Default::default()
}
.build_storage()
.unwrap()
.into();
ext.execute_with(|| System::set_block_number(1));
ext
}

pub fn add_network_disable_commit_reveal(netuid: NetUid, tempo: u16, _modality: u16) {
add_network(netuid, tempo, _modality);
SubtensorPallet::<Runtime>::set_commit_reveal_weights_enabled(netuid, false);
SubtensorPallet::<Runtime>::set_yuma3_enabled(netuid, false);
}

pub fn add_network(netuid: NetUid, tempo: u16, _modality: u16) {
SubtensorPallet::<Runtime>::init_new_network(netuid, tempo);
SubtensorPallet::<Runtime>::set_network_registration_allowed(netuid, true);
FirstEmissionBlockNumber::<Runtime>::insert(netuid, 1);
SubtokenEnabled::<Runtime>::insert(netuid, true);

// make interval 1 block so tests can register by stepping 1 block.
BurnHalfLife::<Runtime>::insert(netuid, 1);
BurnIncreaseMult::<Runtime>::insert(netuid, U64F64::from_num(1));
}

pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) {
SubnetTAO::<Runtime>::set(netuid, tao);
SubnetAlphaIn::<Runtime>::set(netuid, alpha);
}

pub fn register_ok_neuron(
netuid: NetUid,
hotkey_account_id: AccountId,
coldkey_account_id: AccountId,
_start_nonce: u64,
) {
SubtensorPallet::<Runtime>::set_burn(netuid, TaoBalance::from(0));
let reserve: u64 = 1_000_000_000_000;
let tao_reserve = SubnetTAO::<Runtime>::get(netuid);
let alpha_reserve =
SubnetAlphaIn::<Runtime>::get(netuid) + SubnetAlphaInProvided::<Runtime>::get(netuid);

if tao_reserve == 0.into() && alpha_reserve == 0.into() {
setup_reserves(netuid, reserve.into(), reserve.into());
}

// Ensure coldkey has enough to pay the current burn AND is not fully drained to zero.
// This avoids ZeroBalanceAfterWithdrawn in burned_register.
let top_up_for_burn = |netuid: NetUid, cold: AccountId| {
let burn: TaoBalance = SubtensorPallet::<Runtime>::get_burn(netuid);
let burn_u64: TaoBalance = burn;

// Make sure something remains after withdrawal even if ED is 0 in tests.
let ed: TaoBalance = ExistentialDeposit::get();
let min_remaining: TaoBalance = ed.max(1.into());

// Small buffer for safety (fees / rounding / future changes).
let buffer: TaoBalance = 10.into();

let min_balance_needed: TaoBalance = burn_u64 + min_remaining + buffer;

let bal: TaoBalance = SubtensorPallet::<Runtime>::get_coldkey_balance(&cold);
if bal < min_balance_needed {
SubtensorPallet::<Runtime>::add_balance_to_coldkey_account(
&cold,
min_balance_needed - bal,
);
}
};

top_up_for_burn(netuid, coldkey_account_id.clone());

let origin =
<<Runtime as frame_system::Config>::RuntimeOrigin>::signed(coldkey_account_id.clone());
let result = SubtensorPallet::<Runtime>::burned_register(
origin.clone(),
netuid,
hotkey_account_id.clone(),
);

match result {
Ok(()) => {
// success
}
Err(e)
if e == Error::<Runtime>::TooManyRegistrationsThisInterval.into()
|| e == Error::<Runtime>::NotEnoughBalanceToStake.into()
|| e == Error::<Runtime>::ZeroBalanceAfterWithdrawn.into() =>
{
// Re-top-up and retry once (burn can be state-dependent).
top_up_for_burn(netuid, coldkey_account_id.clone());

assert_ok!(SubtensorPallet::<Runtime>::burned_register(
origin,
netuid,
hotkey_account_id.clone()
));
}
Err(e) => {
panic!("Expected Ok(_). Got Err({e:?})");
}
}
SubtensorPallet::<Runtime>::set_burn(netuid, TaoBalance::from(0));
log::info!(
"Register ok neuron: netuid: {netuid:?}, coldkey: {coldkey_account_id:?}, hotkey: {hotkey_account_id:?}"
);
}
59 changes: 59 additions & 0 deletions runtime/tests/subtensor_weights.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
mod common;
use common::new_test_ext;
use common::*;
use frame_support::assert_ok;
use frame_support::dispatch::GetDispatchInfo;
use frame_support::sp_runtime::traits::DispatchTransaction;
use node_subtensor_runtime::{
Runtime, RuntimeCall, RuntimeOrigin,
transaction_payment_wrapper::ChargeTransactionPaymentWrapper,
};
use pallet_subtensor::Pallet as SubtensorPallet;
use subtensor_runtime_common::{AccountId, NetUid};

// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package node-subtensor-runtime --test subtensor_weights -- set_weights_fees_payed_by_coldkey --exact --nocapture
#[test]
fn set_weights_fees_payed_by_coldkey() {
new_test_ext().execute_with(|| {
let hotkey = AccountId::from(common::FOUR_NO_BALANCE);
let coldkey = AccountId::from(common::TWO);
let netuid0 = NetUid::from(1);
let netuid1 = NetUid::from(2);

SubtensorPallet::<Runtime>::set_weights_set_rate_limit(netuid0, 0);

add_network_disable_commit_reveal(netuid0, 1, 0);
add_network_disable_commit_reveal(netuid1, 1, 0);
register_ok_neuron(netuid0, hotkey.clone(), coldkey.clone(), 2143124);
register_ok_neuron(netuid1, hotkey.clone(), coldkey.clone(), 3124124);

let hotkey_balance_before = pallet_balances::Pallet::<Runtime>::free_balance(&hotkey);
let coldkey_balance_before = pallet_balances::Pallet::<Runtime>::free_balance(&coldkey);

let weights_keys: Vec<u16> = vec![0];
let weight_values: Vec<u16> = vec![1];

let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights {
netuid: netuid0,
dests: weights_keys,
weights: weight_values,
version_key: 0,
});

let info = call.get_dispatch_info();
let ext = ChargeTransactionPaymentWrapper::<Runtime>::new(0.into());
assert_ok!(ext.dispatch_transaction(
RuntimeOrigin::signed(hotkey.clone()).into(),
call,
&info,
0,
0,
));

let hotkey_balance_after = pallet_balances::Pallet::<Runtime>::free_balance(&hotkey);
let coldkey_balance_after = pallet_balances::Pallet::<Runtime>::free_balance(&coldkey);

assert_eq!(hotkey_balance_before, hotkey_balance_after);
assert!(coldkey_balance_after < coldkey_balance_before); // Fee paid by coldkey
});
}
36 changes: 36 additions & 0 deletions runtime/tests/transaction_payment_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,39 @@ fn priority_override_applies_with_real_pays_fee() {
assert_eq!(valid_tx.priority, NORMAL_DISPATCH_BASE_PRIORITY);
});
}

// ============================================================
// Coldkey pays for it's hotkey
// ============================================================

// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package node-subtensor-runtime --test transaction_payment_wrapper -- hotkey_origin_charges_coldkey_fee_payer --exact --nocapture
#[test]
fn hotkey_origin_charges_coldkey_fee_payer() {
new_test_ext().execute_with(|| {
let hotkey = signer();
let coldkey = other();

pallet_subtensor::Owner::<Runtime>::insert(&hotkey, &coldkey);

let call = call_remark();

let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(hotkey), &call).unwrap();

assert_eq!(fee_payer(&val), coldkey);
});
}

// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package node-subtensor-runtime --test transaction_payment_wrapper -- hotkey_origin_charges_coldkey_fee_payer_no_association --exact --nocapture
#[test]
fn hotkey_origin_charges_coldkey_fee_payer_no_association() {
new_test_ext().execute_with(|| {
let hotkey = signer();

let call = call_remark();

let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(hotkey.clone()), &call).unwrap();

// No hotkey -> coldkey association, so fee payer is hotkey
assert_eq!(fee_payer(&val), hotkey);
});
}
4 changes: 3 additions & 1 deletion ts-tests/moonwall.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"testFileDir": [
"suites/dev"
],
"runScripts": [],
"runScripts": [
"generate-types.sh"
],
"multiThreads": true,
"reporters": ["basic"],
"foundation": {
Expand Down
Loading
Loading