Skip to content
Merged
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
16 changes: 13 additions & 3 deletions .github/scripts/deploy/approve-upgrade-multisig.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ async function getSudoMultisigProposal(
remainingSignatories, // 2. List of other signatories in lexicographic order
null, // 3. Timepoint (null for the first submission)
multisigProposal.method.hash.toHex(), // 4. The call to be executed (proxy call of runtime upgrade)
{ refTime: 60_000_000_000, proofSize: 10_000 } // 5. Maximum weight as an object
// Must cover the finalizing as_multi's declared weight: its own pinned
// 60e9/10k max_weight argument plus multisig+proxy overhead (measured
// ~65e9/21k on v431). Too-low values fail the executing approval with
// MaxWeightTooLow.
{ refTime: 80_000_000_000, proofSize: 50_000 } // 5. Maximum weight as an object
);

return sudoMultiApprove;
Expand All @@ -50,7 +54,7 @@ async function getSudoMultisigProposal(
remainingSignatories, // 2. List of other signatories in lexicographic order
timePointAndBlockHeight, // 3. Timepoint (null for the first submission)
multisigProposal.method.hash.toHex(), // 4. The call to be executed (proxy call of runtime upgrade)
{ refTime: 60_000_000_000, proofSize: 10_000 } // 5. Maximum weight as an object
{ refTime: 80_000_000_000, proofSize: 50_000 } // 5. Maximum weight as an object
);

return sudoMultiApprove;
Expand All @@ -62,7 +66,13 @@ async function getSudoMultisigProposal(
remainingSignatories, // 2. List of other signatories in lexicographic order
timePointAndBlockHeight, // 3. Timepoint (null for the first submission)
multisigProposal, // 4. The call to be executed (proxy call of runtime upgrade)
{ refTime: 60_000_000_000, proofSize: 10_000 } // 5. Maximum weight as an object
// The executing approval: max_weight must cover the finalizing
// as_multi's declared weight (its pinned 60e9/10k argument plus
// multisig+proxy overhead — measured ~65e9/21k on v431), or the
// chain rejects it with MaxWeightTooLow. The 60e9/10k literal at
// the deploymentMultisigProposal below is different: it is *inside*
// the finalizing call's encoding and must never change.
{ refTime: 80_000_000_000, proofSize: 50_000 } // 5. Maximum weight as an object
);

return sudoMultiApprove;
Expand Down
13 changes: 10 additions & 3 deletions sdk/python/bittensor/cli/commands/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,29 +486,36 @@ async def _submit(client):
if signer_address in approvals:
return None, sudo_layer
others = uh.sorted_other_signatories(sudo_signatories, signer_address)
# The outer max_weight must cover the finalizing call's *declared*
# dispatch weight, which already includes the pinned FINALIZE_WEIGHT
# it carries as its own as_multi argument plus the pallet's base
# overhead — so the pinned constant itself is always too low here.
# Only the finalizing call's encoding must be byte-identical across
# signers; this outer argument is free to be estimated per submission.
max_weight = await uh.finalizing_max_weight(client, finalizing, signer_address)
if sudo_layer is None:
call = calls.Multisig.approve_as_multi(
threshold=sudo_threshold,
other_signatories=others,
maybe_timepoint=None,
call_hash=finalizing.call_hash,
max_weight=uh.FINALIZE_WEIGHT,
max_weight=max_weight,
)
elif len(approvals) < sudo_threshold - 1:
call = calls.Multisig.approve_as_multi(
threshold=sudo_threshold,
other_signatories=others,
maybe_timepoint=sudo_layer["timepoint"],
call_hash=finalizing.call_hash,
max_weight=uh.FINALIZE_WEIGHT,
max_weight=max_weight,
)
else:
call = calls.Multisig.as_multi(
threshold=sudo_threshold,
other_signatories=others,
maybe_timepoint=sudo_layer["timepoint"],
call=finalizing,
max_weight=uh.FINALIZE_WEIGHT,
max_weight=max_weight,
)
result = await client.submit_call(
call, signing, signer="coldkey", wait_for_finalization=False
Expand Down
18 changes: 18 additions & 0 deletions sdk/python/bittensor/cli/upgrade_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,24 @@ async def compose_finalizing_call(
)


async def finalizing_max_weight(client, finalizing, signer_address: str) -> dict:
"""max_weight for the sudo-layer approval that dispatches ``finalizing``.

``pallet_multisig`` rejects the executing ``as_multi`` with
``MaxWeightTooLow`` unless its max_weight covers the wrapped call's
declared dispatch weight — which for the finalizing call is the pinned
``FINALIZE_WEIGHT`` it embeds *plus* the multisig pallet's own overhead.
Estimate the declared weight from the live runtime and pad it 10% so the
approval never lands just under the line.
"""

weight = await client.estimate_weight(finalizing, address=signer_address)
return {
"ref_time": int(weight["ref_time"] * 11 // 10),
"proof_size": int(weight["proof_size"] * 11 // 10),
}


def sorted_other_signatories(signatories: list[str], self_address: str) -> list[str]:
"""Everyone but ``self_address``, sorted by raw account id (chain order)."""
others = [s for s in signatories if s != self_address]
Expand Down
25 changes: 25 additions & 0 deletions sdk/python/bittensor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
)
from .signing import WalletLike
from .snapshot import Snapshot
from .sp_core import ss58_decode

# A generated descriptor is a (container, name) pair (see bittensor/_generated);
# a plain 2-tuple of strings works for items missing from the generated catalog.
Expand Down Expand Up @@ -89,6 +90,20 @@ class BlockInfo:
extrinsics: list = field(repr=False) # decoded values; None for undecodable entries


class _AddressView:
"""Keypair-shaped public view of a bare ss58 address, for fee/weight quotes.

Fee estimation signs with a zeroed signature, so only the public parts are
ever read (``sp_core.ss58_decode`` recovers the public key).
"""

crypto_type = 1 # sr25519

def __init__(self, address: str):
self.ss58_address = address
self.public_key = bytes(ss58_decode(address))


@dataclass
class EpochEvent:
"""An epoch run observed by ``client.wait_for_epoch()``."""
Expand Down Expand Up @@ -480,6 +495,16 @@ async def compose(self, call):
"""
return await self._substrate.compose(call)

async def estimate_weight(self, call, *, address: str) -> dict:
"""The declared dispatch weight ``{ref_time, proof_size}`` of a composed call.

This is what ``pallet_multisig`` compares an executing approval's
``max_weight`` against; needs no key material, only the prospective
signer's ``address`` (fee estimation signs with a zeroed signature).
"""
view = _AddressView(address)
return await self._substrate.estimate_weight(call, view)

async def multisig(self, signatories: list[str], threshold: int) -> Multisig:
"""A handle to the M-of-N multisig account for a signer set.

Expand Down
3 changes: 3 additions & 0 deletions sdk/python/bittensor/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,9 @@ def submit_signature(self, unsigned, signature, **kwargs):
def compose(self, call):
return self._call(self._client.compose(call))

def estimate_weight(self, call, *, address):
return self._call(self._client.estimate_weight(call, address=address))

def multisig(self, signatories, threshold):
multi = self._call(self._client.multisig(signatories, threshold))
return _SyncNamespace(multi, self._call)
Expand Down
Loading