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
34 changes: 33 additions & 1 deletion clones/js-tests/tests/test-proxy-filter-security-regressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const FUND_SOURCE_URI = process.env.PROXY_FILTER_FUND_SOURCE_URI ?? "//Alice";
const FUND_AMOUNT = BigInt(process.env.PROXY_FILTER_FUND_AMOUNT ?? "5000000000000");
const ZERO_HASH = `0x${"00".repeat(32)}`;

const PROXY_TYPES = ["NonFungible", "SwapHotkey", "NonTransfer", "Owner"];
const PROXY_TYPES = ["NonFungible", "SwapHotkey", "NonTransfer", "Owner", "Validate"];

const keyring = new Keyring({ type: "sr25519" });
const fundSource = keyring.addFromUri(FUND_SOURCE_URI);
Expand Down Expand Up @@ -76,6 +76,33 @@ async function main() {
api.tx.adminUtils.sudoSetSnOwnerHotkey(0, replacementHotkey.address)
);

const validateCalls = [
["set weights", api.tx.subtensorModule.setMechanismWeights(0, 0, [], [], 0)],
["serve axon", api.tx.subtensorModule.serveAxon(0, 1, 2130706433, 8091, 4, 0, 0, 0)],
[
"serve axon TLS",
api.tx.subtensorModule.serveAxonTls(0, 1, 2130706433, 8092, 4, 0, 0, 0, "0x"),
],
[
"associate EVM key",
api.tx.subtensorModule.associateEvmKey(
0,
`0x${"11".repeat(20)}`,
1,
`0x${"22".repeat(65)}`
),
],
["set commitment", api.tx.commitments.setCommitment(0, { fields: [] })],
];
for (const [name, call] of validateCalls) {
await expectProxyTypeAllowed(`Validate allows ${name}`, "Validate", call);
}
await expectProxyTypeDenied(
"Validate denies transfer",
"Validate",
balancesTransfer(dummyHotkey.address, 1n)
);

console.log("proxy filter security regressions: ok");
} finally {
await api?.disconnect();
Expand All @@ -101,6 +128,11 @@ async function assertMetadataAvailable() {
// sudo_set_sn_owner_hotkey (call 67); the Owner-proxy denial property
// is the same.
["AdminUtils.sudoSetSnOwnerHotkey", api.tx.adminUtils?.sudoSetSnOwnerHotkey],
["SubtensorModule.setMechanismWeights", api.tx.subtensorModule?.setMechanismWeights],
["SubtensorModule.serveAxon", api.tx.subtensorModule?.serveAxon],
["SubtensorModule.serveAxonTls", api.tx.subtensorModule?.serveAxonTls],
["SubtensorModule.associateEvmKey", api.tx.subtensorModule?.associateEvmKey],
["Commitments.setCommitment", api.tx.commitments?.setCommitment],
].filter(([, value]) => !value);

assert.equal(
Expand Down
14 changes: 14 additions & 0 deletions common/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub enum ProxyType {
SwapHotkey,
SubnetLeaseBeneficiary,
RootClaim,
Validate,
}

impl TryFrom<u8> for ProxyType {
Expand All @@ -67,6 +68,7 @@ impl TryFrom<u8> for ProxyType {
15 => Ok(Self::SwapHotkey),
16 => Ok(Self::SubnetLeaseBeneficiary),
17 => Ok(Self::RootClaim),
18 => Ok(Self::Validate),
_ => Err(()),
}
}
Expand All @@ -93,6 +95,7 @@ impl From<ProxyType> for u8 {
ProxyType::SwapHotkey => 15,
ProxyType::SubnetLeaseBeneficiary => 16,
ProxyType::RootClaim => 17,
ProxyType::Validate => 18,
}
}
}
Expand All @@ -112,6 +115,17 @@ impl Default for ProxyType {
}
}

#[cfg(test)]
mod tests {
use super::ProxyType;

#[test]
fn validate_proxy_type_id_is_stable() {
assert_eq!(u8::from(ProxyType::Validate), 18);
assert_eq!(ProxyType::try_from(18), Ok(ProxyType::Validate));
}
}

/// Extra constraint attached to an allowed call.
#[derive(Clone, PartialEq, Eq, Encode, Decode, Debug, TypeInfo)]
pub enum CallConstraint {
Expand Down
2 changes: 1 addition & 1 deletion docs/query/validator-basket.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ Async is the same surface awaited: `async with bt.Subtensor() as client:`.

## On-chain implementation

- Runtime API [`BetaBasketRuntimeApi.get_validator_basket`](/code/pallets/subtensor/src/staking/basket_views.rs#L109-L117)
- Runtime API [`BetaBasketRuntimeApi.get_validator_basket`](/code/pallets/subtensor/src/staking/basket_views.rs#L155-L163)

Every file is browsable under [/code](/code) exactly as built into the runtime.
2 changes: 1 addition & 1 deletion docs/tx/add-proxy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ only to keys you control or fully trust.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `delegate_ss58` | string | yes | Key that will be allowed to sign for this account. |
| `proxy_type` | string | no | Scope of calls the delegation covers. One of: Any, Owner, NonCritical, NonTransfer, Senate, NonFungible, Triumvirate, Governance, Staking, Registration, Transfer, SmallTransfer, RootWeights, ChildKeys, SudoUncheckedSetCode, SwapHotkey, SubnetLeaseBeneficiary, RootClaim. Triumvirate, Senate, Governance, and RootWeights are deprecated on the current runtime: they deny all calls, so a proxy of those types can dispatch nothing. Prefer the narrowest type that covers your use; Any can do everything the account can, including transfers. |
| `proxy_type` | string | no | Scope of calls the delegation covers. One of: Any, Owner, NonCritical, NonTransfer, Senate, NonFungible, Triumvirate, Governance, Staking, Registration, Transfer, SmallTransfer, RootWeights, ChildKeys, SudoUncheckedSetCode, SwapHotkey, SubnetLeaseBeneficiary, RootClaim, Validate. Triumvirate, Senate, Governance, and RootWeights are deprecated on the current runtime: they deny all calls, so a proxy of those types can dispatch nothing. Prefer the narrowest type that covers your use; Any can do everything the account can, including transfers. |
| `delay` | integer | no | Announcement delay in blocks: the delegate must announce each call and wait this long before executing it, giving you time to veto. 0 executes immediately. |

Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58
Expand Down
2 changes: 1 addition & 1 deletion docs/tx/claim-root-with-hotkey.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,6 @@ pub fn claim_root_with_hotkey(
}
```

Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L837), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L874), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L903).
Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L913), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L950), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L979).

Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/<path>` (index: [`/code/index.json`](/code/index.json)).
2 changes: 1 addition & 1 deletion docs/tx/claim-root.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@ pub fn claim_root(
}
```

Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L837), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L874), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L903).
Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L913), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L950), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L979).

Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/<path>` (index: [`/code/index.json`](/code/index.json)).
2 changes: 1 addition & 1 deletion docs/tx/create-pure-proxy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ the pure proxy and anything it holds.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `proxy_type` | string | no | Scope of calls the delegation covers. One of: Any, Owner, NonCritical, NonTransfer, Senate, NonFungible, Triumvirate, Governance, Staking, Registration, Transfer, SmallTransfer, RootWeights, ChildKeys, SudoUncheckedSetCode, SwapHotkey, SubnetLeaseBeneficiary, RootClaim. Triumvirate, Senate, Governance, and RootWeights are deprecated on the current runtime: they deny all calls, so a proxy of those types can dispatch nothing. Prefer the narrowest type that covers your use; Any can do everything the account can, including transfers. |
| `proxy_type` | string | no | Scope of calls the delegation covers. One of: Any, Owner, NonCritical, NonTransfer, Senate, NonFungible, Triumvirate, Governance, Staking, Registration, Transfer, SmallTransfer, RootWeights, ChildKeys, SudoUncheckedSetCode, SwapHotkey, SubnetLeaseBeneficiary, RootClaim, Validate. Triumvirate, Senate, Governance, and RootWeights are deprecated on the current runtime: they deny all calls, so a proxy of those types can dispatch nothing. Prefer the narrowest type that covers your use; Any can do everything the account can, including transfers. |
| `delay` | integer | no | Announcement delay in blocks: the delegate must announce each call and wait this long before executing it, giving you time to veto. 0 executes immediately. |
| `index` | integer | no | Disambiguator so one signer can create several pure proxies in one block; also part of the derived address. Keep 0 unless batching. |

Expand Down
5 changes: 5 additions & 0 deletions docs/tx/set-weights.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ weights to the subnet's hyperparameters (max-weight clip, u16 quantization,
minimum weight count) and submits via whichever path the subnet runs — a
plain `set_weights` when commit-reveal is off, or a timelock-encrypted
commit (auto-revealed by the chain at the drand reveal round) when it is on.
When the client has `weight_targets` configured, its exact combination of
the signing hotkey and zero-delay `Validate` delegations is submitted with
per-target failure isolation; the chain verifies each proxy grant, and an
empty list is a no-op.
Subnet call sites do not change.
Signed by the hotkey, which must be registered on the subnet. Before
signing it preflights registration and the rate limit, so those failures
are caught fast with the same error the chain would return; the rate-limit
Expand Down
2 changes: 1 addition & 1 deletion docs/tx/stake-into-basket.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ pub fn stake_into_basket(
}
```

Delegates to [`do_stake_into_basket`](/code/pallets/subtensor/src/staking/claim_root.rs#L365).
Delegates to [`do_stake_into_basket`](/code/pallets/subtensor/src/staking/claim_root.rs#L385).

Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/<path>` (index: [`/code/index.json`](/code/index.json)).
2 changes: 1 addition & 1 deletion docs/tx/unstake-all.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub fn claim_root_with_hotkey(
}
```

Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L837), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L874), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L903).
Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L913), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L950), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L979).

`Utility.batch_all` — [`pallets/utility/src/lib.rs#L314`](/code/pallets/utility/src/lib.rs#L308-L362):

Expand Down
19 changes: 19 additions & 0 deletions pallets/utility/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,25 @@ fn force_batch_works() {
});
}

#[test]
fn force_batch_handles_successful_weight_refund() {
new_test_ext().execute_with(|| {
let declared = Weight::from_parts(100, 0);
let actual = Weight::from_parts(75, 0);
let batch_len = 4;
let calls = vec![call_foobar(false, declared, Some(actual)); batch_len];
let call = RuntimeCall::Utility(UtilityCall::force_batch { calls });
let info = call.get_dispatch_info();
let result = call.dispatch(RuntimeOrigin::signed(1));

assert_ok!(result);
assert_eq!(
extract_actual_weight(&result, &info),
info.call_weight - (declared - actual) * batch_len as u64
);
});
}

#[test]
fn none_origin_does_not_work() {
new_test_ext().execute_with(|| {
Expand Down
23 changes: 23 additions & 0 deletions runtime/src/proxy_filters/call_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,29 @@ call_filter_group!(SudoSetCodeCalls, [
where nested(call) == RuntimeCall::System(SystemCall::set_code),
]);

// `Validate`: operate a validator hotkey without granting stake or value movement.
call_filter_group!(
ValidateCalls,
[
RuntimeCall::SubtensorModule(SubtensorCall::serve_axon),
RuntimeCall::SubtensorModule(SubtensorCall::serve_axon_tls),
RuntimeCall::SubtensorModule(SubtensorCall::associate_evm_key),
RuntimeCall::SubtensorModule(SubtensorCall::set_weights),
RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights),
RuntimeCall::SubtensorModule(SubtensorCall::batch_set_weights),
RuntimeCall::SubtensorModule(SubtensorCall::commit_weights),
RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights),
RuntimeCall::SubtensorModule(SubtensorCall::batch_commit_weights),
RuntimeCall::SubtensorModule(SubtensorCall::commit_crv3_mechanism_weights),
RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_weights),
RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_mechanism_weights),
RuntimeCall::SubtensorModule(SubtensorCall::reveal_weights),
RuntimeCall::SubtensorModule(SubtensorCall::reveal_mechanism_weights),
RuntimeCall::SubtensorModule(SubtensorCall::batch_reveal_weights),
RuntimeCall::Commitments(CommitmentsCall::set_commitment),
]
);

// Full inventory of every runtime call, used only by the coverage test that
// checks it against `RuntimeCall` metadata. Nested in three blocks so the
// flattened tuple stays within the `CallFilterMetadata` tuple-impl arity;
Expand Down
27 changes: 26 additions & 1 deletion runtime/src/proxy_filters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ pub(crate) fn proxy_type_filter(proxy_type: &ProxyType, call: &RuntimeCall) -> b
ProxyType::SubnetLeaseBeneficiary => SubnetLeaseAllowed::contains(call),
ProxyType::RootClaim => RootClaimCalls::contains(call),
ProxyType::SudoUncheckedSetCode => SudoSetCodeCalls::contains(call),
ProxyType::Validate => ValidateCalls::contains(call),
ProxyType::Triumvirate
| ProxyType::Senate
| ProxyType::Governance
Expand Down Expand Up @@ -152,7 +153,8 @@ impl InstanceFilter<RuntimeCall> for ProxyType {
| ProxyType::SudoUncheckedSetCode
| ProxyType::SwapHotkey
| ProxyType::SubnetLeaseBeneficiary
| ProxyType::RootClaim,
| ProxyType::RootClaim
| ProxyType::Validate,
) => true,
(ProxyType::Transfer, ProxyType::SmallTransfer) => true,
_ => false,
Expand Down Expand Up @@ -184,6 +186,7 @@ fn proxy_filter_mode(proxy_type: ProxyType) -> FilterMode {
ProxyType::SubnetLeaseBeneficiary => FilterMode::Allow(SubnetLeaseAllowed::call_infos()),
ProxyType::RootClaim => FilterMode::Allow(RootClaimCalls::call_infos()),
ProxyType::SudoUncheckedSetCode => FilterMode::Allow(SudoSetCodeCalls::call_infos()),
ProxyType::Validate => FilterMode::Allow(ValidateCalls::call_infos()),
ProxyType::Triumvirate
| ProxyType::Senate
| ProxyType::Governance
Expand Down Expand Up @@ -398,6 +401,7 @@ mod tests {
ProxyType::SwapHotkey,
ProxyType::SubnetLeaseBeneficiary,
ProxyType::RootClaim,
ProxyType::Validate,
]
.into_iter()
.collect::<BTreeSet<_>>();
Expand Down Expand Up @@ -518,6 +522,27 @@ mod tests {
allowed_calls(ProxyType::SudoUncheckedSetCode),
expected(&["Sudo::sudo_unchecked_weight"])
);
assert_eq!(
allowed_calls(ProxyType::Validate),
expected(&[
"Commitments::set_commitment",
"SubtensorModule::associate_evm_key",
"SubtensorModule::batch_commit_weights",
"SubtensorModule::batch_reveal_weights",
"SubtensorModule::batch_set_weights",
"SubtensorModule::commit_crv3_mechanism_weights",
"SubtensorModule::commit_mechanism_weights",
"SubtensorModule::commit_timelocked_mechanism_weights",
"SubtensorModule::commit_timelocked_weights",
"SubtensorModule::commit_weights",
"SubtensorModule::reveal_mechanism_weights",
"SubtensorModule::reveal_weights",
"SubtensorModule::serve_axon",
"SubtensorModule::serve_axon_tls",
"SubtensorModule::set_mechanism_weights",
"SubtensorModule::set_weights",
])
);
}

// The newer calls that leaked through `main`'s denylists must stay denied
Expand Down
14 changes: 14 additions & 0 deletions runtime/src/transaction_payment_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,20 @@ mod tests {
});
}

#[test]
fn force_batch_of_weight_calls_remains_fee_free() {
let direct = call_set_weights();
let proxied = proxy_call(real_a(), call_set_weights());
assert_eq!(direct.get_dispatch_info().pays_fee, Pays::No);
assert_eq!(proxied.get_dispatch_info().pays_fee, Pays::No);
assert_eq!(
force_batch_call(vec![direct, proxied])
.get_dispatch_info()
.pays_fee,
Pays::No
);
}

#[test]
fn batch_charges_outer_real_when_only_outer_opted_in() {
new_test_ext().execute_with(|| {
Expand Down
23 changes: 23 additions & 0 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,29 @@ These compose with any intent:
On the CLI: `--proxy-for <ss58|wallet>` on every mutation command. Manage
delegations with the `add-proxy` / `remove-proxy` intents and the `proxies` read.

A zero-delay `Validate` proxy needs no subnet code changes. The signing
hotkey is direct and every other target must have granted it a `Validate`
proxy. Targets supplied by subnet code through the client constructor are
merged with `WEIGHT_TARGETS`, so validator operators can add targets without
changing the subnet. Duplicates are removed while constructor order is
preserved; an empty merged set disables weight submission entirely.

```console
WEIGHT_TARGETS=5F...DELEGATE,5F...VALIDATOR_A,5F...VALIDATOR_B
```

The existing subnet call remains unchanged:

```python
result = await client.execute(
bt.SetWeights(netuid=1, weights={0: 0.2, 1: 0.8}), delegate_wallet
)
```

Targets are dispatched with `Utility.force_batch`: one revoked or invalid
target is reported in `result.data["weight_results"]` without preventing the
remaining validators from setting weights.

- **Atomic batch** — several intents in one all-or-nothing extrinsic:

```python
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/bittensor/_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,8 @@ def _result_from_report(self, report: InclusionReport, waited: bool) -> Extrinsi
block_hash=report.block_hash,
extrinsic_id=extrinsic_id,
explorer_url=explorer,
fee=Balance.from_rao(report.total_fee_amount or 0),
events=list(report.triggered_events),
error=ChainError(text, name),
)

Expand Down
Loading