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()
}
}
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 {
match call.is_sub_type() {
Some(pallet_subtensor::Call::set_weights { .. }) => true,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@l0r1s , added the additional validation for the list of extrinsics we want to apply this to.

_ => false,
}
}
}

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()
}
} 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 {
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { beforeAll, expect } from "vitest";
import { describeSuite } from "@moonwall/cli";
import { generateKeyringPair, tao } from "../../../../utils";
import type { ApiPromise } from "@polkadot/api";
import { devForceSetBalance, devSetWeightsTx, devTryAssociateHotkey } from "../../../../utils/dev-helpers.ts";

describeSuite({
id: "00_transaction_payment_wrapper_dev",
title: "Transaction payment wrapper",
foundationMethods: "dev",
testCases: ({ it, context, log }) => {
let api: ApiPromise;

beforeAll(() => {
api = context.polkadotJs();
});

it({
id: "T01",
title: "Check set_weights",
test: async () => {
const coldkey = generateKeyringPair("sr25519");
const hotkey = generateKeyringPair("sr25519");

log(`coldkey: ${coldkey.address}`);
log(`hotkey: ${hotkey.address}`);

const initialBalance = tao(1e10);

log("Set Up");
await devForceSetBalance(api, context, coldkey.address, initialBalance);
await devForceSetBalance(api, context, hotkey.address, initialBalance);
await devTryAssociateHotkey(api, context, coldkey, hotkey.address);

const coldkeyBalanceBefore = (await api.query.system.account(coldkey.address)).data.free.toBigInt();

log("Execute the tx from hotkey, but coldkey will pay");
await devSetWeightsTx(api, context, hotkey, 0, [], [], 0n);

const events = await api.query.system.events();
const feeEvent = events.filter((a) => {
return a.event.method.toString() === "TransactionFeePaid";
});

const hotkeyBalance = (await api.query.system.account(hotkey.address)).data.free.toBigInt();
const coldkeyBalanceAfter = (await api.query.system.account(coldkey.address)).data.free.toBigInt();
// Fees paid by the hotkey
const txFee = feeEvent[0].event.data.actualFee.toBigInt();
expect(txFee).toBeGreaterThan(0n);
expect(coldkeyBalanceAfter).toEqual(coldkeyBalanceBefore - txFee);
expect(hotkeyBalance).toEqual(initialBalance);
},
});
},
});
Loading
Loading