You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
migrate_node_contract (added in #1113, spec 159) has three unrelated preconditions that all return the same NodeNotAvailableToDeploy error, and one of them is about the source node — which the caller never names in the call.
// L358 — (1) SOURCE or DESTINATION is rentedensure!(
!ActiveRentContractForNode::<T>::contains_key(source_node_id)
&& !ActiveRentContractForNode::<T>::contains_key(node_id),Error::<T>::NodeNotAvailableToDeploy);// L368 — (2) DESTINATION is in standbylet node_power = pallet_tfgrid::NodePower::<T>::get(node_id);ensure!(
!node_power.is_standby_phase(),Error::<T>::NodeNotAvailableToDeploy);// L372 — (3) DESTINATION has an extra fee, or the FARM is dedicatedensure!(DedicatedNodesExtraFee::<T>::get(node_id) == 0 && !farm.dedicated_farm,Error::<T>::NodeNotAvailableToDeploy);
The call signature is migrate_node_contract(contract_id, node_id, deployment_hash). The only node the caller names is the destination, so the error reads as "the node you are moving to is not ready". Two of the three causes match that reading. The third does not — and it is the one that fired in practice.
The neighbouring guard already does this correctly:
// L354 — same both-sides shape, but its own specific errorensure!(
!pallet_tfgrid::NodeV3BillingOptOut::<T>::contains_key(source_node_id)
&& !pallet_tfgrid::NodeV3BillingOptOut::<T>::contains_key(node_id),Error::<T>::NodeIsOptedOutOfV3Billing);
So the pallet is internally inconsistent: one both-sides precondition gets a precise error, the three next to it share a generic one.
To Reproduce
Observed on devnet, which has been on spec 159 since block 22730338 (2026-08-20 12:54).
Two council-dispatched migrations of contract 270558 → node 371 failed:
Behaviour is correct — a rent contract flips the cost basis, and the comment above the guard says so explicitly. Only the diagnosis is the problem: nothing in the error, the event, or the call args points at node 396.
For completeness, the sequence resolved on its own rather than being fixed:
The returned error should identify which precondition failed, and on which node.
Suggested mapping — three of the four reuse variants that already exist, so only one new variant is needed:
condition
suggested error
source node has an active rent contract
new, e.g. SourceNodeHasRentContract
destination node has an active rent contract
NodeHasRentContract (existing, [23])
destination node is in standby
NodeNotAvailableToDeploy (keep)
destination has extra fee / farm is dedicated
FarmIsNotDedicated is taken; suggest NodeIsDedicated (new)
This is also the remedy proposed by @sameh-farouk in #914 for the create path:
If this is not enough, we can reuse other errors type like NodeHasActiveContracts, NodeHasRentContract […] If node is rented the error returned is NodeHasRentContract
Additional context
Why #914 does not already cover this.#914 raised the same ambiguity for create_node_contract and was closed as completed because the SDK's AvailableFor node filter made it moot — nodes returned by the filter are guaranteed deployable, so a client never sees the ambiguous error. That resolution does not transfer to migrate_node_contract:
It is RestrictedOrigin (root or 3/5 council), so there is no SDK filter in the loop. A council member picks a node ID by hand.
The failure surfaces only as a raw Council.Executed { result: Err(Module { index, error }) } event. There is no client layer to translate it into a message.
Timing. Error variants are part of the runtime metadata and are decoded positionally by clients. migrate_node_contract is currently devnet-only, so variants can still be added or renumbered without breaking anyone. After it reaches mainnet, that stops being free.
Related, optional.migrate_node_contract reuses ContractUpdated, so migrations cannot be queried directly — they can only be reconstructed by diffing nodeId across successive events, which works only because update_node_contract happens not to touch node_id. A dedicated NodeContractMigrated { contract_id, from_node, to_node } event would make migrations observable and would stay correct if that ever changes. Happy to split this into its own issue if preferred.
Describe the bug
migrate_node_contract(added in #1113, spec 159) has three unrelated preconditions that all return the sameNodeNotAvailableToDeployerror, and one of them is about the source node — which the caller never names in the call.substrate-node/pallets/pallet-smart-contract/src/grid_contract.rs:The call signature is
migrate_node_contract(contract_id, node_id, deployment_hash). The only node the caller names is the destination, so the error reads as "the node you are moving to is not ready". Two of the three causes match that reading. The third does not — and it is the one that fired in practice.The neighbouring guard already does this correctly:
So the pallet is internally inconsistent: one both-sides precondition gets a precise error, the three next to it share a generic one.
To Reproduce
Observed on devnet, which has been on spec 159 since block
22730338(2026-08-20 12:54).Two council-dispatched migrations of contract
270558→ node371failed:Historical state at those blocks shows the destination was clean on all three conditions:
The blocker was the source node,
396:Behaviour is correct — a rent contract flips the cost basis, and the comment above the guard says so explicitly. Only the diagnosis is the problem: nothing in the error, the event, or the call args points at node 396.
For completeness, the sequence resolved on its own rather than being fixed:
Expected
The returned error should identify which precondition failed, and on which node.
Suggested mapping — three of the four reuse variants that already exist, so only one new variant is needed:
SourceNodeHasRentContractNodeHasRentContract(existing,[23])NodeNotAvailableToDeploy(keep)FarmIsNotDedicatedis taken; suggestNodeIsDedicated(new)This is also the remedy proposed by @sameh-farouk in #914 for the create path:
Additional context
Why #914 does not already cover this. #914 raised the same ambiguity for
create_node_contractand was closed as completed because the SDK'sAvailableFornode filter made it moot — nodes returned by the filter are guaranteed deployable, so a client never sees the ambiguous error. That resolution does not transfer tomigrate_node_contract:RestrictedOrigin(root or 3/5 council), so there is no SDK filter in the loop. A council member picks a node ID by hand.Council.Executed { result: Err(Module { index, error }) }event. There is no client layer to translate it into a message.NodeNotAvailableToDeploywhen creating contract #914's three causes were all about the target node. This call adds a fourth that is about a node the caller never named, which is a new failure mode rather than the one Please specify why nodeNodeNotAvailableToDeploywhen creating contract #914 discussed.Timing. Error variants are part of the runtime metadata and are decoded positionally by clients.
migrate_node_contractis currently devnet-only, so variants can still be added or renumbered without breaking anyone. After it reaches mainnet, that stops being free.Related, optional.
migrate_node_contractreusesContractUpdated, so migrations cannot be queried directly — they can only be reconstructed by diffingnodeIdacross successive events, which works only becauseupdate_node_contracthappens not to touchnode_id. A dedicatedNodeContractMigrated { contract_id, from_node, to_node }event would make migrations observable and would stay correct if that ever changes. Happy to split this into its own issue if preferred.