Verifiable computation & batch jobs
The problem
Section titled “The problem”You have computation that is too heavy or too sensitive to trust to one server, and whose results others will rely on: hash chains and commitments, data transformations, simulations, scoring runs. You want several independent machines to compute it, a guarantee they agreed, and a way for anyone to check a result later.
Why Necter fits
Section titled “Why Necter fits”- A committee of miners, picked by sortition the operator cannot steer, runs every job; validators re-execute a random 10 % and every contested job.
- Each result is a receipt hash covering module, function, input, output, events and gas: compare it, store
it, re-check it with
ndsr runyears later. - Scheduled projects get an unpredictable seed per slot, so jobs can be randomized fairly without anyone controlling the randomness.
The live example: Hash Chain Lab
Section titled “The live example: Hash Chain Lab”The testnet’s reference project (hashchain-lab) does exactly this. Every 60 seconds the Hub schedules a task
seeded by the epoch beacon; a committee of 3 miners walks a 64-link Keccak chain from the seed and must agree
on the final link and on the best (lowest) link seen. Miners earn 1 NECTA per unit (85 % miner share).
//! Hash-chain worker for the testnet example project `hashchain-lab`.//!//! Stateless (no storage, no hive.call), so committees of miners can run it. The Hub's//! schedule task source calls `work` with `{"epoch", "project_id", "seed", "seq"}`; the module walks a keccak//! chain from the slot seed and reports the final link plus the best (lowest) link seen, a toy//! proof-of-work whose answer every honest committee member reproduces exactly.
use hivekit::prelude::*;
const DEFAULT_ROUNDS: u64 = 64;const MAX_ROUNDS: u64 = 4096;
fn leading_zero_bits(h: &str) -> u32 { let mut bits = 0; for c in h.trim_start_matches("0x").chars() { let v = c.to_digit(16).unwrap_or(0); if v == 0 { bits += 4; } else { bits += v.leading_zeros() - 28; break; } } bits}
/// `{"seed": "0x…", "seq": n, "rounds"?: n}` (extra keys ignored) → `{"head", "best", "best_zero_bits", "rounds"}`#[hive_export]fn work(input: Value) -> Result<Value, String> { let seed = input["seed"].as_str().ok_or("`seed` must be a hex string")?; let seq = input["seq"].as_u64().unwrap_or(0); let rounds = input["rounds"].as_u64().unwrap_or(DEFAULT_ROUNDS).clamp(1, MAX_ROUNDS); let mut link = hash(format!("{seed}:{seq}").as_bytes()); let mut best = link.clone(); for _ in 1..rounds { link = hash(link.as_bytes()); if link < best { best = link.clone(); } } Ok(json!({ "head": link, "best": best, "best_zero_bits": leading_zero_bits(&best), "rounds": rounds }))}
hive_module!(work);[package]name = "hashchain-worker"version = "1.0.0"edition = "2021"publish = false
[lib]crate-type = ["cdylib"]path = "src/lib.rs"
[dependencies]hivekit = { path = "../necter-sdk/hivekit-rs" }
[profile.release]opt-level = "s"lto = truecodegen-units = 1panic = "abort"Reproduce a job yourself
Section titled “Reproduce a job yourself”hivec build # → dist/hashchain_worker.hbcndsr run dist/hashchain_worker.hbc work --gas 2000000 --input \ '{"epoch":497631,"project_id":"0x84588f60bd2190a6c7929d655010de448e15d22d61ec8021b29ef2c56a553743","seed":"0x1f0b4c6f2d8e3a9b7c5d1e0f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e5f7a9b","seq":12}'{"best":"0x01ba17c8fccc8f2b61755ec8b61883fe1c863b702a9ba48048813f5613ddbbd2","best_zero_bits":7,"head":"0x1829bc42032738828d410524849976d92660b9b21471f722647919f60bfa329c","rounds":64}gas_used: 89288. Built from the SDK revision deployed on the testnet, the artifact’s address is exactly the
live worker’s, 0xa99c85c5c7c08f302256cad48b51d7c363a6fcea09d5d48e570c711eed8e6d90 (GET /v1/modules/{address}
reports "stateless": true). A different SDK revision gives a different address but the same outputs.
Watch it run
Section titled “Watch it run”P=0x84588f60bd2190a6c7929d655010de448e15d22d61ec8021b29ef2c56a553743curl -s "https://testnet-rpc.necter.network/v1/projects/$P/rounds?limit=5"curl -s "https://testnet-rpc.necter.network/v1/projects/$P" # economics, stats, minersEach round shows its committee size, votes, agreeing count, whether it was audited, gas and receipt hash.
How it was published
Section titled “How it was published”- Built and uploaded the worker (
POST /v1/modules). - Manifest with a schedule task source:
{"kind": "schedule", "function": "work", "interval_secs": 60, "gas_limit": 2000000}, committee size 3 (min 3, no backups),round_secs60,epoch_secs3600,per-unitrewards of 1 NECTA, daily emission 24,000 NECTA, split 8500/1000/500,min_collateral10 NECTA, all device classes and engines. The complete document is on The project manifest. prepare→personal_sign→POST /v1/developers/projects→ gaslessregister.- Operator review →
listed. createVault(developer-paid gas) andfundwith 100,000 NECTA.
Building your own batch jobs
Section titled “Building your own batch jobs”Swap the hash chain for your computation:
- Submitted jobs (
task_source: {"kind": "api"}): your backend posts each chunk of data as a task input (≤ 1 MiB) and gets back the agreed output. Split large datasets into chunks and combine the per-chunk digests (for example in a Merkle tree) to get one verifiable digest for the whole run. - Generated jobs (
schedule): derive the job fromseed,epochandseq, for example a simulation run or a random audit sample of a published dataset. - Return digests of large results instead of the results themselves when outputs would be big; outputs are limited to 1 MiB.
| Hash Chain Lab | |
|---|---|
| Gas per job | ~89k → 1 unit per agreeing member |
| Jobs per day | 1,440 (one per minute) |
| Committee | 3 |
| Max miner units per day | 4,320 → at 1 NECTA/unit gross, 3,672 NECTA to miners (85 %) |
| Daily emission cap | 24,000 NECTA (not reached at this rate) |
Heavier jobs earn more units per round (ceil(gas_used / 1,000,000)), so pricing scales with work done.