diff --git a/.github/scripts/deploy/approve-upgrade-multisig.js b/.github/scripts/deploy/approve-upgrade-multisig.js index ab7f6501e3..4de82ae7d1 100644 --- a/.github/scripts/deploy/approve-upgrade-multisig.js +++ b/.github/scripts/deploy/approve-upgrade-multisig.js @@ -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; @@ -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; @@ -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; diff --git a/sdk/python/bittensor/cli/commands/upgrade.py b/sdk/python/bittensor/cli/commands/upgrade.py index 5426668270..3947660b6c 100644 --- a/sdk/python/bittensor/cli/commands/upgrade.py +++ b/sdk/python/bittensor/cli/commands/upgrade.py @@ -486,13 +486,20 @@ 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( @@ -500,7 +507,7 @@ async def _submit(client): 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( @@ -508,7 +515,7 @@ async def _submit(client): 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 diff --git a/sdk/python/bittensor/cli/upgrade_helpers.py b/sdk/python/bittensor/cli/upgrade_helpers.py index 1b81c87454..ccbf2b4cba 100644 --- a/sdk/python/bittensor/cli/upgrade_helpers.py +++ b/sdk/python/bittensor/cli/upgrade_helpers.py @@ -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] diff --git a/sdk/python/bittensor/client.py b/sdk/python/bittensor/client.py index a7cf165caf..5a9e048ab0 100644 --- a/sdk/python/bittensor/client.py +++ b/sdk/python/bittensor/client.py @@ -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. @@ -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()``.""" @@ -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. diff --git a/sdk/python/bittensor/sync.py b/sdk/python/bittensor/sync.py index 2eb0e82b39..d7d9e4d225 100644 --- a/sdk/python/bittensor/sync.py +++ b/sdk/python/bittensor/sync.py @@ -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)