Milestone 3 · Network
Nuvex maintainers
The predicate, check by check
Selection has to be a function of public inputs, because a private backend that picks a fulfiller cannot be audited from the chain. In Milestone 3 that function is an eligibility test inside fulfill, and every condition is read from accounts.
- The requester did not name the fulfiller, and the node authority signs.
- The node account was created at a slot strictly earlier than the request.
- The VRF public key was set at registration, is not all zeros, and update_node cannot replace it.
- The capability mask includes the VRF bit, and the proof verifies under the stored key.
- Node status is Active, and stake is at or above min_stake.
- last_heartbeat is non-zero and the current slot is inside last_heartbeat plus heartbeat_timeout_slots.
- The registry's unstake_cooldown_slots and heartbeat_timeout_slots are both non-zero.
First come, not weighted
There is no assignment step and no draw. Among the nodes that satisfy the predicate, the first transaction to land wins, and a second fulfilment of the same request is rejected.
“VRF fulfillment is first-come among nodes that meet the ADR 0003 predicate. There is no weighted lottery.”
Reputation exists as a counter, not as a weight. It increases by one through note_fulfillment, which only the oracle-core protocol PDA may call, and a slash sets it to zero.
What fulfilment records
The values the predicate checked are written onto the request as assigned_node, assigned_stake and assigned_heartbeat. They are the numbers at the moment of the check rather than a balance someone can read later, which is what makes the decision recomputable by an outside observer.
- job_type
- VRF
- requester
- your program
- input
- 32-byte seed
- callback
- consumer program
- max_fee
- recorded, not charged
Three statuses and one cooldown
- Registered: the stake may sit below the minimum, and the node can neither fulfil nor heartbeat.
- Active: a deposit reached min_stake.
- Unstaking: the whole stake stays locked until the cooldown ends, and heartbeat and fulfil are rejected.
Withdraw returns the accounted stake after the cooldown and sets the node back to Registered. A node cannot deposit while unstaking. The cooldown and the heartbeat window are registry fields that must each sit between 1 and 150,000 slots, which is the existing request-timeout safety bound rather than a chosen economic duration. Slash takes an amount named by the slash authority, during the cooldown if need be, and there is no automatic percentage.
Nothing in this tree submits a fulfilment
The predicate is implemented on-chain, and the off-chain half that would race for it is not. The node process runs a health and metrics server on /health, /live, /ready and /metrics. It does not load signing keys, connect to an RPC or fulfil jobs.
“A node process in this repository still does not submit transactions.”
The same holds one layer up: the Rust SDK can prove a VRF output on the host, the TypeScript SDK's submitRequest throws on purpose, and the CLI reports the milestone each command is waiting on. The threat model counts that as the current mitigation for a node that trusts a lying RPC, which is an accurate description of a gap, not a feature.
- /healthok
- /metricsexposed
- VRF keynot loaded
- fulfilmentsnot submitted
The part a predicate cannot fix
Eligibility says who may submit. It does not say whose output gets chosen when one operator controls several eligible keys, because every proof under every one of those keys is valid.
“An operator who funds several keys can still submit the output they prefer.”
The cost of another identity is another deposit of at least min_stake, the registry authority sets that number, and zero is allowed. Stake-weighted assignment would need an on-chain node index that does not exist and a weight that nobody has justified, so the limitation is published rather than closed.

