Skip to content

feat(smart-contract): migrate a node contract to another node in the same farm - #1113

Open
sameh-farouk wants to merge 3 commits into
developmentfrom
feat/migrate-node-contract
Open

feat(smart-contract): migrate a node contract to another node in the same farm#1113
sameh-farouk wants to merge 3 commits into
developmentfrom
feat/migrate-node-contract

Conversation

@sameh-farouk

@sameh-farouk sameh-farouk commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #1112.

Adds migrate_node_contract(contract_id, node_id, deployment_hash) at call index 22, council/root only. Moves a live node contract to another node in the same farm without cancelling it, so a farmer can empty a machine and power it down without destroying the tenant's contract.

It moves the booking, not the workload. The source node deprovisions its copy on its next reconciliation and deletes the disks, so getting the data off first is the caller's job. What survives that cancellation does not is the contract itself — id, billing continuity, payee, reserved IPs — and therefore something to redeploy against.

Refuses

Non-Created contracts, cross-farm moves, either node opted out of v3 billing or under a rent contract, and a destination that is standby, dedicated, or already holds the same deployment hash.

Also in here

Both report handlers now skip a stale entry instead of aborting the whole batch. They already did this for a contract that does not exist; a contract that exists on another node was handled the opposite way, aborting and charging. That asymmetry was unreachable until this feature — nothing else leaves a live contract pointing elsewhere.

Docs

docs/architecture/0027-migrate-node-contract.md for the reasoning, docs/misc/migrate_node_contract.md for the runbook.

Tests: 473 passing incl. 19 new unit tests and a council-driven integration case.

…same farm

Adds `migrate_node_contract(contract_id, node_id, deployment_hash)` at call
index 22, restricted to `RestrictedOrigin` (root or 3/5 council). It moves a live
node contract to another node in the same farm without cancelling it, so a farmer
can empty a machine and power it down without destroying the tenant's contract.

The extrinsic bills the contract first, settling at the source node's cost basis,
then re-reads it because billing can mutate or remove it. It moves the entry in
`ContractIDByNodeIDAndHash` behind an equality guard, since `update_node_contract`
never enforced hash uniqueness and a key may already point at a different live
contract. `ContractPaymentState.last_updated_seconds` is stamped unconditionally.

It moves the booking, not the workload. The source node deprovisions its copy on
its next reconciliation and deletes the disks, so preserving data is the caller's
responsibility before calling. See docs/architecture/0027 for the full reasoning
and docs/misc/migrate_node_contract.md for the operational runbook.

Also brings the two report handlers into line with each other. Both already
skipped a report naming a contract that does not exist, silently and for free;
a report naming a contract that exists but sits on another node was handled the
opposite way, aborting the whole batch and charging for it. Both now skip and
log. That asymmetry was unreachable before this change, since cancellation
removes the contract and the missing-contract guards catch it.

Weight is parameterised on `max(source, destination)` active-contract count,
following the convention established in 526158f. The measured weights.rs still
needs regenerating via workflow 060 before merge.

Fixes a pre-existing bug in the integration harness where TMP_DIR was "\tmp",
which Python reads as TAB + "mp".
@sameh-farouk
sameh-farouk requested a review from LeeSmet as a code owner August 19, 2026 13:11
The NodeContractResources section opened by explaining that the tenant is not
over-billed by a migration. That is true but it documents a non-event: billing
continues at the same quantity on the same schedule, so describing it at length
mostly plants the doubt it then answers.

What survives is the warning, which three reviewers of this change independently
tried to "fix" by clearing the entry. Rewritten to stand on its own so the
dangling reference to the removed paragraph goes with it.

Also drops "retiring the 2012 generation means 122" from the context. The number
carried no unit, and the sentence already lands with 74 contracts across 29 twins.
…into the code

The ADR read as an implementation walkthrough: it narrated what the extrinsic does
in execution order, re-telling mechanics that already carry comments at their own
sites. That duplicates the code and goes stale the moment either side moves.

Reorganised around the eight decisions actually taken, each with its reasoning and
what it costs. Preconditions move into their own section, since an operator wants
them as a list rather than buried in rationale. Nothing is dropped: the call_index
23 forward constraint, the transaction_version reasoning, the ZOS-forced ordering,
certification repricing and the weight ceiling are all still here.

The "do not clear NodeContractResources" warning becomes a comment in
_migrate_node_contract, next to the mutation where someone would be tempted. Three
reviewers of this change proposed clearing it; the warning belongs where that edit
would be written, not in a document they may not open.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support updating the node ID on the node contract

1 participant