Qubic Decentralization Report

← Dashboard

How it works

The concept behind the report — what is measured, why those metrics, and where attribution stops being provable.

Working concept for a tooling project that produces a standing "how decentralized is Qubic" report from self-reported and on-chain data, and exposes it as an API that explorers can embed.

Status: draft v0.4.0 · Owner: (Qubic community project) · Language: English (spec is for the Qubic community / bounty reviewers; happy to keep a German copy alongside)


1. Background — what CFB actually asked for

In the #computor-operator discussion, Come-from-Beyond (CFB) framed a specific, buildable deliverable:

"Prepare a report showing revenue metrics and their dynamics. And clustering with number of computors in each cluster." "Explorers should add this report to their sites. It's a very important report, showing how decentralized Qubic is."

Two things sit underneath that request:

The problem — collusion / Sybil among computors. Qubic's network is run by 676 Computors. On paper that looks like 676 independent operators. In practice a single entity (a pool) can control many Computor slots at once and coordinate them. That is a textbook Sybil situation: one entity, many identities. AndreiBLR's "we see smoke and know there's a fire, but the investigators are the ones who set it" is exactly this — the signals of concentration are visible, but the parties who could investigate are the same parties benefiting from it.

The proposed defense — self-reporting. CFB's earlier point:

"Qubic was the first implementing such theoretical anti-Sybil technique as self-reporting … Let's make it look more solid by using it to the fullest."

Self-reporting is a recognized-but-rarely-implemented anti-Sybil idea: operators voluntarily declare which identities (Computor slots) they control. Qubic already has the raw material for this because pools publish which of their members hold slots. "Use it to the fullest" = turn that self-declared ownership into a public, continuously updated decentralization metric that lives on the explorers.

CFB's own stance on the stakes (paraphrased from the thread): if Computors collude, the worst-case is suboptimal service / performance, not a broken chain — and if the community can see the concentration clearly, it can decide for itself. So the tool's job is not to accuse anyone; it is to make concentration measurable and visible.

A bounty of ~2B QU (Eko 1B + Broms 1B) has been offered for a report that gets accepted and embedded by explorers.


2. What the tool produces

A single logical artifact — the Decentralization Report — available in three forms:

  1. A machine-readable API (/report/latest, /report/{epoch}, plus sub-resources). This is the primary deliverable: explorers (qubic.org explorer, qubic.li, jetski's ANN explorer, etc.) call it and render it however they like. We provide the data; the explorer is the consumer.
  2. A reference dashboard — our own front-end that renders the same data, including the animated views (see §6). Doubles as the "reference implementation" so an explorer can copy the presentation if they want.
  3. A periodic static snapshot (JSON + rendered PNG/SVG) per epoch, so the report is archivable and citable even if the live service is down.

Every number in the report is reproducible from public data and ships with its inputs, so nobody has to trust us — an explorer or a skeptic can re-run it.


3. Data sources

LayerSourceWhat we getRole
Consensus / slotsQubic RPC 2.0 (rpc.qubic.org) + core node dataComputor list per epoch (676 IDs), tick data, quorum infoGround truth
RevenueRPC / archiver, full transaction tracking (§4.3)Per-Computor revenue per epoch, epoch-scoped and paginated to exhaustionGround truth
On-chain linkageQubic ledger via RPCPayout destinations, fund flows between identities, the payout graphPrimary attribution
Self-reportingPool APIs & public declarations (qubic.li and others), plus a curated registry in this repoWhich slots a pool/operator claimsLabels + supplements linkage
Behavioral (optional)Solution-submission timing/patterns where observableSignals that flag possible undeclared clustersFlagging only

Exact endpoint names are verified against the live RPC during implementation; see docs/DATA_SOURCES.md (to be filled in during the data-mapping step).


4. The analytical engines

4.1 Revenue metrics & dynamics

Per epoch, per Computor: revenue earned; aggregated to per-cluster totals. Concentration measured with standard, defensible indices so the result is not a matter of opinion:

  • Gini coefficient of revenue across operators (0 = perfectly even, 1 = one entity takes all).
  • Herfindahl–Hirschman Index (HHI) of operator revenue share.
  • Top-N share (e.g. share held by the largest 1 / 3 / 5 operators).
  • Nakamoto coefficient — how many operators must collude to control >⅓ / >½ of slots or revenue (the headline "how decentralized" number).

"Dynamics" = all of the above as a time series over epochs, so trends (is concentration rising or falling?) are visible.

4.2 Clustering — slots → operators

This is the core. On-chain linkage is the default attribution layer, not a fallback. Self-reporting is what operators claim; on-chain payout linkage is what the ledger shows. Both run on every epoch, independently, and the report publishes both plus their delta. A slot is only "unattributed" when the ledger itself shows no link — never merely because nobody filed a self-report.

The three layers, and what each is allowed to conclude:

  1. On-chain linkage (default, always computed). Derived from the full payout graph: every epoch-boundary distribution to a computor identity is tracked to where the funds go next. Slots whose payouts converge on a shared destination, or that are funded from a common source, are one economic owner. This runs with no registry at all and is what produces the baseline clustering.
  2. Self-reported (declaration layer, per CFB). Pools declare their slots into a versioned registry. This labels clusters (a linkage cluster becomes "Qubic.li Pool" instead of "Linked (ABCD…)") and can merge slots the ledger has not yet linked. It never splits what the chain has linked — a declaration cannot un-prove a payout path.
  3. Behavioral fingerprints (flagging only). Correlated submission timing / source patterns can flag slots that behave as one — surfaced as "possible undeclared cluster," never as a hard accusation.

Confidence levels now reflect that ordering:

LevelMeaning
declared+linkedSelf-reported and confirmed by the payout graph — strongest
linkedProven on-chain, not (yet) declared — the undeclared concentration
declaredDeclared, no on-chain confirmation yet — trust-only
flaggedBehavioral correlation only, no proof
unattributedThe ledger shows no link and nobody declared it — a genuine unknown

Why this ordering matters for the headline numbers. Treating every undeclared slot as its own operator does not produce a neutral result — it produces a systematically optimistic one, because an unlinked singleton inflates the operator count and pushes Nakamoto, Gini and HHI toward "more decentralized than reality." The previous design made that the default and the on-chain layer an optional enrichment; that is backwards, and the report is stated with a linkage-coverage figure so a reader can see how much of the 676 is actually resolved rather than assumed.

Output per cluster: number of computors, revenue, share of Top-451, confidence, the evidence used, and a declared-vs-detected delta that quantifies the "smoke."

4.3 Revenue derivation — full transaction tracking

Concentration numbers are only as good as the revenue figures underneath them, and this is where independent reimplementations diverge. The derivation is therefore specified exactly and made auditable rather than left to a best-effort scan:

  • Epoch-scoped. Payouts are attributed to the epoch they settle for, using the epoch's tick range (initialTick → next epoch's initialTick), not to whenever a transfer happens to appear. Qubic pays with a one-epoch lag, so an unscoped scan mixes two epochs.
  • Complete. The transfer history is read with full pagination to exhaustion. A truncated first page silently under-counts the largest operators, which biases concentration downward.
  • Anchored on a verified arbitrator identity. The payout source is confirmed against boundary-tick transfers and pinned in the repo with the tick evidence, not carried as an unverified constant.
  • Cross-checkable. Each epoch's derived revenue ships with the totals and tick range it was derived from, so a third party can recompute it and see exactly where a divergence comes from — which epoch, which tick window, which transfers.
  • Reconciled. Derived totals are checked against the known distribution model (per-computor cap after the operator fee, performance/slashing factors). A mismatch is reported in the payload as a warning instead of being averaged away.

Where an epoch cannot be derived completely, it is marked partial and excluded from the headline metrics rather than published as if it were solid.

Which source, in practice. The public RPC does not expose these payouts at all (DATA_SOURCES §6.2), so the pipeline reads them from a Bob node, which keeps the full event log including the virtual end-epoch tick where the protocol credits every computor:

  1. Bob's end-epoch log (primary). One call per epoch returns every settlement transfer. Verified on epoch 228: 676 of 676 computors paid, 178 477 462 349 QU, with history back to at least epoch 220 — so the report starts with real history rather than from zero.
  2. Balance deltas (fallback). Where no Bob node is reachable, revenue is the change in each computor's cumulative incomingAmount across the epoch boundary, from snapshots we take ourselves. Self-contained, but only forward from the first boundary observed.

Point QDR_BOB_URL at your own node rather than depending on a public one.

4.3.1 Why the payout source proves nothing about ownership

All 676 payouts come from the same null address — the credit is protocol emission, not a wallet. So "shares a payout source" is true of the whole network and must produce no linkage; and computors' own outgoing transfers are uniformly 1 000 000 QU burns to that same address. Both facts are traps for a naive payout-graph implementation (one cost us a bug that reported 100 % on-chain coverage while proving nothing). Linkage therefore ignores null/burn destinations, needs at least two computors sharing a destination, and discards destinations used by more than half the network.

Current finding: no on-chain linkage between computors is provable from the public ledger today. The report states that as linkage_coverage: 0 instead of implying independence, and the dashboard warns that the Nakamoto figure is an upper bound on decentralization. Self-reports are what would sharpen it — which is exactly CFB's point about using self-reporting "to the fullest".

4.4 Slot transitions — surviving operator exits and re-identification

Every layer above describes a single epoch. That is not enough for the question the community actually asks when a pool shuts down: where did its slots go? An operator that leaves does not take its 676-slot share with it — the slots are re-issued, and the concentration effect of that hand-over is invisible to any per-epoch snapshot, because each epoch on its own looks internally consistent.

This is the failure mode a per-epoch report has by construction. Concretely, when a large pool closes there are three outcomes and they are numerically indistinguishable in a snapshot:

  1. Slots go to genuinely new, independent operators → real decentralization gain.
  2. Slots are absorbed by the remaining large operators → concentration rises sharply.
  3. The same operator returns under fresh identities and no new declaration → concentration is unchanged in reality but appears to fall, because the successor slots enter the report as unlinked singletons.

Case 3 is the dangerous one: a departing operator re-entering under new IDs makes the headline numbers improve at the exact moment the network has learned nothing. A report that cannot separate case 1 from case 3 will systematically flatter the network after every pool exit, which is precisely when readers rely on it most.

The epoch-over-epoch diff is therefore a first-class output, not a derived view. For each epoch boundary the report computes the identity delta against the previous epoch and classifies it:

ClassMeaning
retainedIdentity present in both epochs
departedPresent in epoch n-1, absent in n
enteredAbsent in epoch n-1, present in n
succeededAn entered slot linked by evidence to a departed operator

Successor detection. An entered identity is tested against recently departed operators using the same ledger evidence §4.2 already relies on, so this adds a dimension rather than a new trust assumption:

  • Shared payout destination — the new identity's distributions converge on an address the departed cluster also paid into. This is the strongest signal and reuses the payout graph unchanged.
  • Funding lineage — the new identity's slot was funded from, or its early outflows return to, the departed cluster's known addresses.
  • Continuity in timing — the entered slot resumes the departed cluster's submission pattern at the boundary tick, without the ramp-up an unrelated new operator shows.

A match promotes the cluster to succeeded and the operator identity is carried across the boundary, so an operator's history is continuous even when every one of its computor IDs changed. Without this, a rename silently resets an operator's entire time series to zero, and the roll-up rewards it for doing so.

Confidence is inherited conservatively. A succeeded link is evidence of continuity, never a declaration: it is reported at linked confidence at best, and drops to flagged when only the timing signal supports it. Cluster continuity claimed on timing alone is always surfaced as a hypothesis with its evidence attached, so a reader can disagree with a specific link rather than with the whole number.

Churn is published as a metric in its own right. Per epoch boundary the report states how many slots changed hands, what share of Top-451 revenue moved, how much of the movement resolved to succeeded versus genuinely entered, and — the honest caveat — how much of the delta remains unexplained. A high unexplained churn is itself the finding: it means the report's confidence in that epoch's headline numbers should be lower, and it is stated that way rather than smoothed over.

Announced exits are recorded before they happen. The self-reporting registry gains an optional per-operator status (active / winding_down / closed, with an effective epoch), so a known shutdown is on record ahead of the boundary and the diff for that epoch can be interpreted rather than reconstructed after the fact. This turns a pool closure from a retrospective puzzle into a pre-registered event with an expected slot count to account for.


5. API shape (draft)

GET /report/latest                 → summary: epoch, #clusters, Nakamoto coeff, Gini, HHI, top clusters
GET /report/{epoch}                → same, historical
GET /clusters/{epoch}              → full cluster list: id, label, computor_count, revenue, share, confidence
GET /computors/{epoch}            → per-slot: id, cluster_id, revenue, evidence
GET /metrics/timeseries           → concentration indices across epochs (feeds the charts)
GET /transitions/{epoch}          → slot diff vs. epoch-1: retained / departed / entered / succeeded,
                                    churn share of Top-451 revenue, unexplained-delta figure (§4.4)
GET /operators/{id}/history       → one operator across epochs, continuous through ID changes (§4.4)
GET /report/{epoch}/snapshot.json → frozen, archivable snapshot

Design principles: read-only, cacheable, CORS-open (explorers embed it client-side), versioned (/v1/), and every response carries generated_at, data_sources, and a reproducible: true block naming the inputs.


5.1 Persistence — a store, not a rebuild

The report is a time series, so history is the product, not a by-product. Rebuilding every answer from the RPC on each request (the previous design) has three defects: it makes the API slow and dependent on upstream uptime, it loses any epoch the RPC no longer serves, and it means two runs can silently disagree with no record of which was which. Overwriting api/sample/*.json on each build discarded the past outright.

Storage model. This follows the pattern already proven in the sibling Qubic projects (qubic_doge_stats, qubic_spotlight): a single embedded, file-backed database in a mounted volume, located via the same DATA_DIR environment variable this repo's RPC cache already honours — no database server to operate, and the file is trivially copyable and archivable. Those projects use LiteDB because they are .NET; the direct Python equivalent is SQLite (standard library, no new dependency, same single-file-in-a-volume semantics). We adopt the architecture, not the library.

Concretely, the parts carried over from qubic_doge_stats:

  • an UpdateLive() / FinalizeEpoch() split — the running epoch is refreshed on every poll, a closed epoch is finalized once (this is exactly the sealed-vs-live rule below);
  • upsert with dedupe rather than blind insert, so a re-poll never duplicates rows;
  • "once correctly set, immutable" — a finalized epoch's values are not overwritten by a later poll;
  • a backfill service for filling history and for re-deriving under a new code version;
  • polling workers on independent intervals, so a slow revenue pull never blocks the cheap tick/epoch pointer.

Tables:

TableHolds
epochsone row per epoch: tick range, status (sealed / partial / live), when it was first and last computed
computor_revenueper epoch, per identity: derived revenue + the tick window it came from
transfersthe tracked payout transfers backing that revenue — the audit trail that makes a divergence explainable
clustersper epoch: cluster membership, confidence, evidence
reportsthe finished report JSON per epoch, with code_version
linkagethe payout-graph edges, so linkage is incrementally extendable rather than recomputed from zero

Sealed vs. live — the core rule. A closed epoch is immutable: computed once, written, and served from the store forever after. Its inputs cannot change, so recomputing it only risks drift. The current epoch is explicitly not sealed — it is recomputed on a short interval (and on demand) and served with status: "live" plus the timestamp of the last refresh, so consumers always see the running epoch's latest state and can tell it apart from settled history.

An epoch is sealed only when its successor has started and its revenue derivation is complete (full pagination, reconciliation passed). An epoch that closes with gaps stays partial and is retried, rather than being frozen wrong.

Recompute and versioning. When the pipeline's logic changes (a corrected arbitrator, a better linkage rule), sealed epochs are not silently rewritten: a backfill runs under a new code_version, and the store keeps the prior computation. That way a number that changes has a visible reason — which matters directly for the "our numbers don't match" problem.

Ingest is incremental. Raw pulls stay cached (data/raw/) as today, but the derived layer is now written once and read many times. A restart, an RPC outage, or an explorer hammering the API all read the store; only the live epoch touches the network.

API impact. /v1/report/{epoch} becomes a store lookup and can serve any historical epoch, not just whatever snapshot was last built. Responses carry status (sealed / partial / live), computed_at, and code_version. The static api/sample/*.json snapshots stay only as a cold-start fallback for a fresh install with an empty database.


5.1.1 Two clocks: what is live, and what is not

Measured against the live network: the tick advances ~1.6/s, while the computor list, revenue and clustering change once per epoch — once a week, Wednesday 12:00 UTC to Wednesday 12:00 UTC (the tick count per epoch varies, 1.08M-2.29M ticks; the week does not). Polling the analysis every minute would burn RPC budget to produce an identical answer for a week.

So the service runs two cadences:

  • /v1/pulse — tick, epoch progress, tick quality, active addresses. Cheap, cached ~10 s, designed to be polled every 15 s. This is what the dashboard animates.
  • The report — recomputed only when the epoch turns (the dashboard watches the pulse's epoch number rather than polling the report on a timer).

The running epoch also has no revenue yet — it is credited when the epoch closes — so the headline report is the newest settled epoch, with the running one shown separately as the live panel. Presenting the running epoch as the report would show an empty one while a complete one sat right behind it.


5.2 Built for a moving network

The network does not hold still: epochs arrive continuously, pools appear, merge and disappear, the analysis layer will gain new evidence types, and even "676 computors" is a current constant rather than a law. Anything pinned to today's shape becomes a lie later, silently. The rules that keep this honest:

  • No network constant is hardcoded. Slot counts, epoch numbers and operator counts are read from the data on every render. In particular the UI language files carry no numbers: strings use placeholders ({slots}, {epoch}, {operators}) that are filled from the live payload, so a translation written today still tells the truth when the network changes. A language file must never be tied to one epoch's values.
  • New operators need no code change. A pool added to the registry, or a cluster the payout graph newly reveals, simply appears in the next epoch's report — clustering is data-driven, never an enumerated list.
  • Unknown values degrade honestly. A confidence level this dashboard version has never seen is rendered neutrally under its own name, never silently relabelled as an existing level, and it is not counted as on-chain-linked. Being wrong in the direction of "we cannot confirm this" is the safe direction.
  • Old readers, new data. Report payloads are additive: consumers ignore fields they do not know, so an explorer running an older embed keeps working when the report grows.
  • History is bounded in transit, not on disk. The store keeps every epoch forever, but the default API views return a recent window (52 epochs for the timeseries, 26 for the dashboard bundle) so responses stay a constant size as history accumulates. The full range stays available on request (?epochs=).
  • Forward compatibility is tested, not assumed. tests/test_forward_compat.py runs the analysis at 100 / 676 / 1 000 / 2 048 slots, across a growing epoch range, with pools added mid-history and an unknown confidence level injected.

6. Dashboard & animation

The reference dashboard renders the report and — where there is genuinely something to animate — animates it. Animation is used only where motion carries meaning, not decoration:

  • Cluster evolution over epochs — an animated treemap / bubble chart where each bubble is an operator, size = slots or revenue; play the epochs and watch clusters grow, shrink, merge, or split. This makes "one pool creeping toward the Top-451 threshold" visible at a glance.
  • Nakamoto coefficient timeline — an animated line that draws across epochs, with the danger thresholds (⅓, ½) marked.
  • Fund-flow / linkage graph — a force-directed graph of slots and payout links that settles into clusters, so on-chain linkage is literally watchable.
  • Declared vs. detected — a transition that morphs the "official" self-reported clustering into the detected one, so the gap ("the smoke") is the animation itself.

Static fallbacks are always available (each animated view has a still image), because the explorers may embed the static form.

6.1 Dashboard UX requirements

  • Layout / visual language: take cues from existing Qubic community front-ends (e.g. Qubic Dividends) so the report feels native to the ecosystem — same general card/table/dark aesthetic, so an explorer can drop it in without a jarring restyle.
  • Dark / light mode: a toggle in the header. Dark is the default (matches the ecosystem). Fully theme-aware — both modes are first-class, not an afterthought.
  • Language switch DE / EN: all UI strings run through an i18n layer with German and English. English is the default; German is a full first-class translation. Structured so more languages can be added later (simple key → string dictionaries).
  • Persistence: both the theme choice and the language choice are saved to localStorage and restored on next visit. Reads/writes are wrapped in try/catch so the page still renders correctly if storage is unavailable (private mode, blocked cookies), falling back to defaults (EN + dark).

7. Why this satisfies the ask

  • Delivers exactly the two things CFB named: revenue metrics + dynamics, and clustering with computor counts per cluster.
  • Built on self-reporting as the primary layer, which is the anti-Sybil technique CFB wants used "to the fullest," while adding on-chain/behavioral layers to quantify the gap.
  • Ships as an API for explorers to embed, which is his stated distribution requirement.
  • Neutral and reproducible — it measures concentration, it does not accuse; anyone can re-run it, which answers the "no qualified/neutral investigator" problem.

7.1 Community feedback and what changed (v0.2)

Four pieces of feedback on the v0.1 draft, and how the concept answers each. The first three turned out to point at the same weakness: a solid metrics layer sitting on an unproven data layer. The fourth points at a different one: a report that only ever looks at one epoch at a time.

1. "Mine doesn't converge with the numbers you have — I've had full transaction tracking running for about 6 epochs." (Kevarms)

Correct — and investigating it turned up something bigger than a bug in our pagination.

v0.1 derived revenue with a single unpaginated pass over the arbitrator's transfers, with no epoch/tick scoping and an unverified arbitrator identity. §4.3 specifies the fix (epoch tick windows, exhaustive pagination, reconciliation). But when that was run against the live RPC on 2026-09-07, it returned zero, and the reason is fundamental (full evidence in docs/DATA_SOURCES.md §6):

  • the arbitrator identity we carried paid 0 of 676 computors — it was simply wrong;
  • scanning ~4 500 transactions per computor across the entire epoch-228 window found no inbound payments at all. What computor identities actually emit is a stream of amount = 0 transactions to the null address — those are solution submissions, not payouts;
  • yet /v1/balances reports 0.5–1.6 B QU of inbound value per computor. The value is real, but the transfers carrying it are not exposed by the transaction endpoints.

Conclusion: computor revenue is credited by protocol-level emission, not by a transfer the public RPC exposes. "Track the arbitrator's payouts" cannot work no matter how completely it paginates — the records are not there. That is almost certainly the root of the divergence, and it means neither implementation can be checked against the other until both state which source they use.

What we do instead (§4.3, implemented): revenue is measured as the delta of each computor's cumulative incomingAmount across the epoch boundary, using balance snapshots we take ourselves — which is exactly what the store (§5.1) exists for. Verified working against the live chain. It is self-contained, reproducible, and starts producing correct numbers from the next boundary forward.

Two things still worth doing, and the second needs Kevarms directly:

  1. Request a qubic.li Score API token — the fastest route to historical revenue and an independent cross-check.
  2. Compare methods, not just numbers. Since the public transfer endpoints do not carry these payments, a 6-epoch dataset that converges must be reading a different source (a node-level feed, a pool API, or balance deltas as we now do). Establishing which is more valuable than arguing about totals — and it is the actual acceptance test for this layer.

Alongside this, three concrete endpoint corrections came out of the same session (wrong paths, a 250-row pagination cap, and where epoch tick windows actually live); all are recorded in docs/DATA_SOURCES.md §6.1 so the next implementer does not repeat them.

2. "Good tooling, rough input; on-chain linkage should be the default, not the fallback bucket labeled unattributed." (Jure Ursic Cergol)

Accepted as an architectural correction. In v0.1 apply_onchain_linkage() existed but was never called by the report path, so the only real attribution came from a registry whose computors lists were deliberately empty — meaning every one of the 676 slots became its own "unattributed" singleton and the headline decentralization numbers described 676 fictional independent operators. §4.2 inverts the layering: the payout graph is computed on every epoch and produces the baseline clustering; self-reporting labels and supplements it but can never split what the chain has linked. "Unattributed" now means the ledger shows no link, not nobody filed a form, and the report states its linkage coverage so a reader can see how much of the network is resolved versus assumed.

3. "Don't you need to persist anything?" (admin)

Yes. v0.1 rebuilt every response from the RPC and overwrote its only snapshot files on each build, so history was lost and no two runs were comparable. §5.1 introduces a SQLite store with the explicit split the project needs: the current epoch is always recomputed live so the running epoch is never stale, while closed epochs are sealed and served from the store so history is immutable, archivable, and no longer dependent on the RPC still serving old epochs. Logic changes trigger a versioned backfill rather than a silent rewrite — so when a number changes, there is a recorded reason.

4. "Apool closing — where are the IDs they were using going?" (Vaintor)

The concept had no answer, because every layer in §4.2 reasons within a single epoch. A pool shutting down is the exact event a per-epoch report cannot interpret: the departing operator's slots are re-issued, and whether they land with new independent operators, get absorbed by the remaining large pools, or come back under the same operator's fresh identities, each of those produces an internally consistent snapshot. Worse, the third case makes the headline numbers improve — the successor slots enter as unlinked singletons and inflate the operator count — so the report would have flattered the network at precisely the moment it should have raised a flag.

§4.4 adds the missing dimension: an epoch-over-epoch identity diff (retained/departed/entered/succeeded) as a first-class output, successor detection that reuses the existing payout graph to carry an operator's identity across an ID change, a published churn metric that includes an explicit unexplained share, and an optional status field in the registry so an announced shutdown is on record before the boundary rather than reconstructed afterwards. The question is also a concrete acceptance test: when Apool's exit epoch closes, the report must be able to state where those slots went, and say plainly how much of the movement it could not explain.


8. Where the report stands

Two of the three layers are finished and running on live data. The third — deciding who operates a slot — is the one the chain cannot answer, and it is where the project now depends on the community rather than on more code.

Working, on live data:

  • Revenue per computor slot, from a Bob node's end-epoch log. Epochs 225-228 are sealed with 676/676 computors paid each. (§4.3)
  • Concentration and its dynamics: Gini, HHI, Nakamoto ⅓/½, top-N share, per epoch and as a time series. (§3)
  • A store that keeps itself current: history backfilled on first start, each epoch sealed as it closes, and any epoch computed by an older code version re-derived on deploy, so a correctness fix retires the figures it corrects. (§5.1)
  • An API and dashboard that never show a figure nobody measured: no sample fallback, an unfilled store answers 503, and every number is labelled for what it counts.

Open — and this is the finding, not a defect:

Clustering runs on every epoch and currently yields one cluster: 676 unattributed slots, because neither attribution layer resolves anything.

  • On-chain linkage (§4.2) is implemented and finds nothing provable. Every computor is credited by the same null address — protocol emission, not a wallet — and their outgoing transfers are uniformly 1,000,000 QU burns to that same address. No transfer graph links two computors. Reported as linkage_coverage: 0 rather than presented as independence.
  • Self-reporting (§4.1), CFB's own anti-Sybil point, is empty: no pool has declared its slots. The registry, its schema and its validator are in place and waiting.

The report therefore measures revenue per slot and labels it that way throughout: a Nakamoto coefficient of 222 means 222 of 676 slots, not 222 independent operators. Where one operator holds several slots the true concentration is higher, and the page says so.

That is a measurement in its own right — self-reporting adoption is currently zero — and the tool is the infrastructure for changing it. A pool opens a pull request against data/self_reporting/pools.json; the operator view (treemap, per-operator table, operator-level Nakamoto) turns itself on for that pool with no code change, and git history is the audit trail. That is "using self-reporting to the fullest" made concrete.


Generated from docs/CONCEPT.md — edit the concept, not this page. Read the source document on GitHub

Wie es funktioniert

Das Konzept hinter dem Report — was gemessen wird, warum diese Metriken, und wo die Zuordnung aufhört, beweisbar zu sein.

Arbeitskonzept für ein Tool, das aus self-reported und On-chain-Daten einen laufenden Bericht „Wie dezentral ist Qubic?" erzeugt und ihn als API bereitstellt, die Explorer einbinden können.

Status: Entwurf v0.4.0 · Owner: (Qubic-Community-Projekt) · Sprachen: Diese Datei ist die deutsche Fassung; die englische liegt unter docs/CONCEPT.md. Beide werden synchron gehalten.


1. Hintergrund — was CFB konkret verlangt hat

In der Diskussion im #computor-operator-Kanal hat Come-from-Beyond (CFB) eine konkrete, umsetzbare Aufgabe formuliert:

„Prepare a report showing revenue metrics and their dynamics. And clustering with number of computors in each cluster." „Explorers should add this report to their sites. It's a very important report, showing how decentralized Qubic is."

Dahinter stehen zwei Dinge:

Das Problem — Kollusion / Sybil unter den Computors. Das Qubic-Netzwerk wird von 676 Computors betrieben. Auf dem Papier sieht das nach 676 unabhängigen Betreibern aus. In der Praxis kann eine einzelne Partei (ein Pool) viele Computor-Slots gleichzeitig kontrollieren und koordinieren. Das ist ein klassischer Sybil-Fall: eine Entität, viele Identitäten. AndreiBLRs Bild „Wir sehen Rauch und wissen, dass es brennt, aber die Ermittler sind die, die das Feuer gelegt haben" beschreibt genau das — die Anzeichen der Konzentration sind sichtbar, aber die, die ermitteln könnten, profitieren selbst davon.

Die vorgeschlagene Abwehr — Self-Reporting. CFBs früherer Punkt:

„Qubic was the first implementing such theoretical anti-Sybil technique as self-reporting … Let's make it look more solid by using it to the fullest."

Self-Reporting ist eine anerkannte, aber selten umgesetzte Anti-Sybil-Idee: Betreiber deklarieren freiwillig, welche Identitäten (Computor-Slots) sie kontrollieren. Qubic hat das Rohmaterial dafür bereits, weil Pools veröffentlichen, welche ihrer Mitglieder Slots halten. „Use it to the fullest" heißt: diese selbst deklarierte Zugehörigkeit in eine öffentliche, laufend aktualisierte Dezentralisierungs-Kennzahl verwandeln, die auf den Explorern lebt.

CFBs Haltung zu den Risiken (sinngemäß aus dem Thread): Wenn Computors kolludieren, ist der schlimmste Fall suboptimaler Service / Performance, keine kaputte Chain — und wenn die Community die Konzentration klar sehen kann, kann sie selbst entscheiden. Aufgabe des Tools ist also nicht, jemanden anzuklagen, sondern Konzentration messbar und sichtbar zu machen.

Ein Bounty von ~2 Mrd. QU (Eko 1B + Broms 1B) ist für einen Report ausgelobt, der akzeptiert und von Explorern eingebunden wird.


2. Was das Tool erzeugt

Ein logisches Artefakt — den Decentralization Report — in drei Formen:

  1. Eine maschinenlesbare API (/report/latest, /report/{epoch} plus Unter-Ressourcen). Das ist das primäre Ergebnis: Explorer (qubic.org-Explorer, qubic.li, jetskis ANN-Explorer usw.) rufen sie ab und stellen sie beliebig dar. Wir liefern die Daten; der Explorer ist der Konsument.
  2. Ein Referenz-Dashboard — unser eigenes Front-End, das dieselben Daten rendert, inklusive der animierten Ansichten (siehe §6). Dient zugleich als „Referenzimplementierung", die ein Explorer bei Bedarf übernehmen kann.
  3. Ein periodischer statischer Snapshot (JSON + gerendertes PNG/SVG) pro Epoche, damit der Report archivierbar und zitierbar bleibt, selbst wenn der Live-Dienst ausfällt.

Jede Zahl im Report ist aus öffentlichen Daten reproduzierbar und wird mit ihren Inputs ausgeliefert, sodass niemand uns vertrauen muss — ein Explorer oder ein Skeptiker kann alles nachrechnen.


3. Datenquellen

EbeneQuelleWas wir bekommenRolle
Konsens / SlotsQubic RPC 2.0 (rpc.qubic.org) + Core-Node-DatenComputor-Liste pro Epoche (676 IDs), Tick-Daten, Quorum-InfosGrundwahrheit
RevenueRPC / Archiver, vollständiges Transaction-Tracking (§4.3)Revenue pro Computor pro Epoche, epochenscharf und vollständig paginiertGrundwahrheit
On-chain-VerknüpfungQubic-Ledger via RPCAuszahlungsziele, Fund-Flows zwischen Identitäten, der Payout-GraphPrimäre Zuordnung
Self-ReportingPool-APIs & öffentliche Deklarationen (qubic.li u. a.) plus ein kuratiertes Registry in diesem RepoWelche Slots ein Pool/Betreiber für sich beanspruchtBeschriftet + ergänzt die Verknüpfung
Verhalten (optional)Timing/Muster der Solution-Submissions, soweit beobachtbarSignale, die mögliche nicht deklarierte Cluster markierenNur Markierung

Die genauen Endpoint-Namen werden während der Umsetzung gegen die Live-RPC verifiziert; siehe docs/DATA_SOURCES.md (wird im Data-Mapping-Schritt gefüllt).


4. Die Analyse-Engines

4.1 Revenue-Metriken & Dynamik

Pro Epoche, pro Computor: erzieltes Revenue; aggregiert zu Cluster-Summen. Konzentration wird mit gängigen, gut begründbaren Indizes gemessen, damit das Ergebnis keine Meinungssache ist:

  • Gini-Koeffizient des Revenues über die Betreiber (0 = perfekt gleich, 1 = eine Entität bekommt alles).
  • Herfindahl-Hirschman-Index (HHI) der Revenue-Anteile der Betreiber.
  • Top-N-Anteil (z. B. Anteil der größten 1 / 3 / 5 Betreiber).
  • Nakamoto-Koeffizient — wie viele Betreiber kolludieren müssen, um >⅓ / >½ der Slots oder des Revenues zu kontrollieren (die zentrale „Wie dezentral"-Zahl).

„Dynamik" = alle obigen Werte als Zeitreihe über die Epochen, sodass Trends (steigt oder sinkt die Konzentration?) sichtbar werden.

4.2 Clustering — Slots → Betreiber

Das ist der Kern. On-chain-Verknüpfung ist die Standard-Zuordnungsebene, kein Auffangbecken. Self-Reporting ist das, was Betreiber behaupten; die On-chain-Verknüpfung über den Payout-Graph ist das, was das Ledger zeigt. Beide laufen in jeder Epoche unabhängig voneinander, und der Report veröffentlicht beide plus ihre Differenz. Ein Slot gilt nur dann als „unattributed", wenn das Ledger selbst keine Verknüpfung zeigt — nie bloß deshalb, weil niemand ein Self-Reporting eingereicht hat.

Die drei Ebenen und was jede schlussfolgern darf:

  1. On-chain-Verknüpfung (Standard, immer berechnet). Abgeleitet aus dem vollständigen Payout-Graph: Jede Auszahlung am Epochenende an eine Computor-Identität wird weiterverfolgt. Slots, deren Auszahlungen auf demselben Ziel zusammenlaufen oder die aus einer gemeinsamen Quelle gespeist werden, sind ein wirtschaftlicher Eigentümer. Das läuft ganz ohne Registry und erzeugt das Basis-Clustering.
  2. Self-reported (Deklarationsebene, gemäß CFB). Pools deklarieren ihre Slots in ein versioniertes Registry. Das beschriftet Verknüpfungs-Cluster (aus „Linked (ABCD…)" wird „Qubic.li Pool") und kann Slots zusammenführen, die die Chain noch nicht verknüpft hat. Es trennt aber nie, was die Chain verknüpft hat — eine Deklaration kann einen Auszahlungspfad nicht widerlegen.
  3. Verhaltens-Fingerprints (nur Markierung). Korrelierte Submission-Timings/-Quellen können Slots markieren — als „mögliches nicht deklariertes Cluster", nie als harte Anschuldigung.

Konfidenzstufen bilden diese Reihenfolge ab:

StufeBedeutung
declared+linkedSelf-reported und durch den Payout-Graph bestätigt — am stärksten
linkedOn-chain bewiesen, (noch) nicht deklariert — die nicht deklarierte Konzentration
declaredDeklariert, noch keine On-chain-Bestätigung — reine Vertrauensbasis
flaggedNur Verhaltenskorrelation, kein Beweis
unattributedDas Ledger zeigt keine Verknüpfung und niemand hat deklariert — echt unbekannt

Warum die Reihenfolge für die Kennzahlen entscheidend ist. Jeden nicht deklarierten Slot als eigenen Betreiber zu zählen ist nicht neutral, sondern systematisch zu optimistisch: Ein unverknüpfter Einzel-Slot bläht die Betreiberzahl auf und schiebt Nakamoto, Gini und HHI Richtung „dezentraler als die Realität". Der bisherige Entwurf machte genau das zum Standard und die On-chain-Ebene zur optionalen Anreicherung — das ist verkehrt herum. Der Report weist deshalb eine Verknüpfungs-Abdeckung aus, damit erkennbar ist, wie viel der 676 tatsächlich aufgelöst und wie viel nur angenommen ist.

Ausgabe pro Cluster: Anzahl Computors, Revenue, Anteil an Top-451, Konfidenz, verwendete Evidenz und ein deklariert-vs-erkannt-Delta, das den „Rauch" quantifiziert.

4.3 Revenue-Ableitung — vollständiges Transaction-Tracking

Konzentrationszahlen sind nur so gut wie das Revenue darunter, und genau hier laufen unabhängige Implementierungen auseinander. Die Ableitung ist deshalb exakt spezifiziert und überprüfbar, statt auf ein Best-Effort-Scannen zu setzen:

  • Epochenscharf. Auszahlungen werden der Epoche zugeordnet, für die sie abgerechnet werden — über deren Tick-Bereich (initialTick → initialTick der Folgeepoche), nicht danach, wann ein Transfer zufällig auftaucht. Qubic zahlt mit einer Epoche Verzögerung; ein Scan ohne Bereichsgrenzen vermischt zwei Epochen.
  • Vollständig. Die Transfer-Historie wird bis zur Erschöpfung paginiert. Eine abgeschnittene erste Seite unterzählt still gerade die größten Betreiber — was die Konzentration nach unten verzerrt.
  • Auf einer verifizierten Arbitrator-Identität verankert. Die Auszahlungsquelle wird gegen die Boundary-Tick-Transfers bestätigt und mit Tick-Nachweis im Repo festgehalten, statt als ungeprüfte Konstante mitgeschleppt zu werden.
  • Gegenprüfbar. Jede Epoche liefert den Tick-Bereich und die Summen mit, aus denen sie abgeleitet wurde — so kann eine dritte Partei nachrechnen und sieht genau, woher eine Abweichung kommt: welche Epoche, welches Tick-Fenster, welche Transfers.
  • Abgeglichen. Die abgeleiteten Summen werden gegen das bekannte Verteilungsmodell geprüft (Obergrenze pro Computor nach Betreibergebühr, Performance-/Slashing-Faktoren). Eine Abweichung wird als Warnung im Payload gemeldet, statt weggemittelt zu werden.

Wo eine Epoche nicht vollständig abgeleitet werden kann, wird sie als partial markiert und aus den Kennzahlen herausgehalten, statt so veröffentlicht zu werden, als wäre sie belastbar.

Welche Quelle konkret. Die öffentliche RPC macht diese Auszahlungen überhaupt nicht sichtbar (DATA_SOURCES §6.2). Die Pipeline liest sie deshalb von einem Bob-Node, der das vollständige Event-Log vorhält — inklusive des virtuellen End-Epoch-Ticks, an dem das Protokoll jeden Computor gutschreibt:

  1. Bobs End-Epoch-Log (primär). Ein Aufruf pro Epoche liefert jeden Abrechnungstransfer. Für Epoche 228 verifiziert: 676 von 676 Computors bezahlt, 178 477 462 349 QU, mit Historie mindestens zurück bis Epoche 220 — der Report startet also mit echter Historie statt bei null.
  2. Balance-Deltas (Fallback). Wo kein Bob-Node erreichbar ist, ist Revenue die Änderung des kumulativen incomingAmount jedes Computors über die Epochengrenze, aus selbst genommenen Snapshots. In sich geschlossen, aber erst ab der ersten beobachteten Grenze.

QDR_BOB_URL auf den eigenen Node richten, statt von einem öffentlichen abzuhängen.

4.3.1 Warum die Auszahlungsquelle nichts über Eigentum aussagt

Alle 676 Auszahlungen kommen von derselben Null-Adresse — die Gutschrift ist Protokoll-Emission, keine Wallet. „Teilt sich eine Auszahlungsquelle" trifft also auf das gesamte Netz zu und darf keine Verknüpfung erzeugen. Und die ausgehenden Transfers der Computors sind durchweg 1 000 000 QU Burns an dieselbe Adresse. Beides sind Fallen für eine naive Payout-Graph-Implementierung (eine davon kostete uns einen Bug, der 100 % On-Chain- Abdeckung meldete und dabei nichts bewies). Die Verknüpfung ignoriert deshalb Null-/Burn- Ziele, verlangt mindestens zwei Computors mit gemeinsamem Ziel und verwirft Ziele, die mehr als die Hälfte des Netzes nutzt.

Aktueller Befund: Aus dem öffentlichen Ledger ist heute keine On-Chain-Verknüpfung zwischen Computors nachweisbar. Der Report weist das als linkage_coverage: 0 aus, statt Unabhängigkeit zu suggerieren, und das Dashboard warnt, dass der Nakamoto-Wert eine Obergrenze der Dezentralisierung ist. Self-Reports würden das schärfen — genau CFBs Punkt, Self-Reporting „to the fullest" zu nutzen.

4.4 Slot-Übergänge — Betreiber-Abgänge und Re-Identifikation überstehen

Alle bisherigen Schichten beschreiben eine einzelne Epoche. Das genügt nicht für die Frage, die die Community tatsächlich stellt, wenn ein Pool dichtmacht: Wohin gehen seine Slots? Ein Betreiber, der geht, nimmt seinen Anteil an den 676 Slots nicht mit — die Slots werden neu vergeben, und der Konzentrationseffekt dieser Übergabe ist in keiner Einzelepochen-Momentaufnahme sichtbar, weil jede Epoche für sich betrachtet in sich stimmig aussieht.

Das ist die bauartbedingte Schwachstelle eines Per-Epochen-Reports. Konkret: Schließt ein großer Pool, gibt es drei Ausgänge, die in einer Momentaufnahme numerisch nicht unterscheidbar sind:

  1. Slots gehen an echte, unabhängige neue Betreiber → realer Dezentralisierungs*gewinn*.
  2. Slots werden von den verbleibenden großen Betreibern aufgesogen → Konzentration steigt deutlich.
  3. Derselbe Betreiber kommt unter frischen Identitäten und ohne neue Selbstauskunft zurück → die Konzentration ist real unverändert, scheint aber zu sinken, weil die Nachfolge-Slots als unverknüpfte Einzelstücke in den Report eingehen.

Fall 3 ist der gefährliche: Ein abgehender Betreiber, der unter neuen IDs wieder einsteigt, lässt die Kennzahlen genau in dem Moment besser aussehen, in dem das Netzwerk nichts dazugelernt hat. Ein Report, der Fall 1 nicht von Fall 3 trennen kann, beschönigt das Netzwerk systematisch nach jedem Pool-Abgang — also genau dann, wenn Leser sich am meisten auf ihn verlassen.

Der Epochen-zu-Epochen-Diff ist deshalb ein erstklassiges Ergebnis, keine abgeleitete Ansicht. Für jede Epochengrenze berechnet der Report das Identitäts-Delta gegenüber der Vorepoche und klassifiziert es:

KlasseBedeutung
retainedIdentität in beiden Epochen vorhanden
departedIn Epoche n-1 vorhanden, in n nicht mehr
enteredIn Epoche n-1 nicht vorhanden, in n neu
succeededEin entered-Slot, der per Nachweis einem departed-Betreiber zuzuordnen ist

Nachfolger-Erkennung. Eine entered-Identität wird gegen kürzlich departed-Betreiber geprüft — mit denselben Ledger-Nachweisen, auf die sich §4.2 ohnehin stützt. Das fügt eine Dimension hinzu, statt eine neue Vertrauensannahme einzuführen:

  • Gemeinsames Auszahlungsziel — die Ausschüttungen der neuen Identität laufen auf eine Adresse zu, in die auch das abgegangene Cluster gezahlt hat. Das stärkste Signal; es nutzt den Payout-Graph unverändert.
  • Funding-Herkunft — der Slot der neuen Identität wurde aus den bekannten Adressen des abgegangenen Clusters finanziert, oder seine frühen Abflüsse laufen dorthin zurück.
  • Kontinuität im Timing — der neue Slot nimmt an der Grenz-Tick das Einreichungsmuster des abgegangenen Clusters wieder auf, ohne die Anlaufphase, die ein unabhängiger neuer Betreiber zeigt.

Ein Treffer hebt das Cluster auf succeeded und trägt die Betreiber-Identität über die Grenze hinweg, sodass die Historie eines Betreibers auch dann durchgängig bleibt, wenn sämtliche seiner Computor-IDs gewechselt haben. Ohne das setzt eine Umbenennung die gesamte Zeitreihe eines Betreibers still auf null zurück — und die Auswertung belohnt ihn dafür.

Konfidenz wird konservativ vererbt. Eine succeeded-Verknüpfung ist ein Kontinuitäts- nachweis, keine Selbstauskunft: Sie wird höchstens mit Konfidenz linked ausgewiesen und fällt auf flagged, wenn sie nur vom Timing-Signal getragen wird. Eine allein auf Timing gestützte Cluster-Kontinuität wird immer als Hypothese samt Nachweis ausgewiesen, damit ein Leser einer einzelnen Verknüpfung widersprechen kann statt der ganzen Zahl.

Churn wird als eigene Metrik veröffentlicht. Pro Epochengrenze nennt der Report, wie viele Slots den Besitzer gewechselt haben, welcher Anteil des Top-451-Revenue sich bewegt hat, wie viel davon sich zu succeeded statt zu echtem entered auflösen ließ — und, als ehrlicher Vorbehalt, wie viel des Deltas unerklärt bleibt. Ein hoher unerklärter Churn ist selbst der Befund: Er bedeutet, dass das Vertrauen in die Kennzahlen dieser Epoche geringer sein sollte, und genau so wird er ausgewiesen statt weggeglättet.

Angekündigte Abgänge werden erfasst, bevor sie eintreten. Die Selbstauskunfts-Registry erhält ein optionales status pro Betreiber (active / winding_down / closed, mit einer effective-Epoche). So ist eine bekannte Schließung vor der Grenze aktenkundig und der Diff dieser Epoche kann interpretiert statt im Nachhinein rekonstruiert werden. Damit wird aus einer Pool-Schließung ein vorab registriertes Ereignis mit einer erwarteten Slot-Zahl, die aufgehen muss — statt eines nachträglichen Rätsels.


5. API-Form (Entwurf)

GET /report/latest                 → Zusammenfassung: Epoche, #Cluster, Nakamoto-Koeff., Gini, HHI, Top-Cluster
GET /report/{epoch}                → dasselbe, historisch
GET /clusters/{epoch}              → vollständige Cluster-Liste: id, label, computor_count, revenue, share, confidence
GET /computors/{epoch}            → pro Slot: id, cluster_id, revenue, evidence
GET /metrics/timeseries           → Konzentrationsindizes über die Epochen (speist die Charts)
GET /transitions/{epoch}          → Slot-Diff ggü. Epoche-1: retained / departed / entered / succeeded,
                                    Churn-Anteil am Top-451-Revenue, unerklärtes Delta (§4.4)
GET /operators/{id}/history       → ein Betreiber über die Epochen, durchgängig trotz ID-Wechsel (§4.4)
GET /report/{epoch}/snapshot.json → eingefrorener, archivierbarer Snapshot

Design-Prinzipien: read-only, cachebar, CORS-offen (Explorer binden client-seitig ein), versioniert (/v1/); jede Antwort trägt generated_at, data_sources und einen reproducible: true-Block, der die Inputs benennt.


5.1 Persistenz — ein Speicher statt Neuberechnung

Der Report ist eine Zeitreihe, Historie ist also das Produkt und kein Nebenprodukt. Jede Antwort bei jedem Request neu aus der RPC zu bauen (bisheriger Entwurf) hat drei Mängel: Es macht die API langsam und von der Verfügbarkeit der Gegenstelle abhängig, es verliert jede Epoche, die die RPC nicht mehr ausliefert, und zwei Läufe können still unterschiedliche Ergebnisse liefern, ohne dass festgehalten wäre, welcher welcher war. Das Überschreiben von api/sample/*.json bei jedem Build hat die Vergangenheit schlicht weggeworfen.

Speichermodell. Das folgt dem Muster, das sich in den Schwesterprojekten (qubic_doge_stats, qubic_spotlight) bereits bewährt hat: eine einzelne eingebettete, dateibasierte Datenbank in einem gemounteten Volume, aufgelöst über dieselbe DATA_DIR-Umgebungsvariable, die der RPC-Cache dieses Repos schon nutzt — kein Datenbankserver zu betreiben, und die Datei ist trivial kopier- und archivierbar. Jene Projekte nutzen LiteDB, weil sie .NET sind; das direkte Python-Äquivalent ist SQLite (Standardbibliothek, keine neue Abhängigkeit, gleiche Semantik „eine Datei im Volume"). Wir übernehmen die Architektur, nicht die Bibliothek.

Konkret aus qubic_doge_stats übernommen:

  • die Trennung UpdateLive() / FinalizeEpoch() — die laufende Epoche wird bei jedem Poll aufgefrischt, eine abgeschlossene genau einmal finalisiert (exakt die Live-vs-Sealed-Regel unten);
  • Upsert mit Dedupe statt blindem Insert, damit ein erneuter Poll nie Zeilen dupliziert;
  • „einmal korrekt gesetzt, unveränderlich" — die Werte einer finalisierten Epoche werden von einem späteren Poll nicht überschrieben;
  • ein Backfill-Service zum Füllen der Historie und zum Neuableiten unter neuer Code-Version;
  • Polling-Worker auf unabhängigen Intervallen, damit ein langsamer Revenue-Pull nie den günstigen Tick-/Epochen-Zeiger blockiert.

Tabellen:

TabelleInhalt
epochseine Zeile pro Epoche: Tick-Bereich, Status (sealed / partial / live), wann zuerst und zuletzt berechnet
computor_revenuepro Epoche und Identität: abgeleitetes Revenue + das Tick-Fenster, aus dem es stammt
transfersdie verfolgten Auszahlungs-Transfers hinter diesem Revenue — der Audit-Trail, der eine Abweichung erklärbar macht
clusterspro Epoche: Cluster-Zugehörigkeit, Konfidenz, Evidenz
reportsder fertige Report als JSON pro Epoche, mit code_version
linkagedie Kanten des Payout-Graphen, damit Verknüpfung inkrementell wächst statt jedes Mal bei null zu beginnen

Sealed vs. live — die Kernregel. Eine abgeschlossene Epoche ist unveränderlich: einmal berechnet, geschrieben und danach für immer aus dem Speicher ausgeliefert. Ihre Eingangsdaten können sich nicht mehr ändern, eine Neuberechnung birgt also nur das Risiko von Drift. Die laufende Epoche ist ausdrücklich nicht versiegelt — sie wird in kurzem Intervall (und auf Anforderung) neu berechnet und mit status: "live" plus dem Zeitpunkt der letzten Aktualisierung ausgeliefert. So sehen Konsumenten immer den aktuellen Stand der laufenden Epoche und können ihn von der abgeschlossenen Historie unterscheiden.

Eine Epoche wird erst versiegelt, wenn ihre Nachfolgerin begonnen hat und ihre Revenue-Ableitung vollständig ist (vollständige Paginierung, Abgleich bestanden). Eine Epoche, die mit Lücken schließt, bleibt partial und wird erneut versucht, statt falsch eingefroren zu werden.

Neuberechnung und Versionierung. Ändert sich die Logik der Pipeline (korrigierter Arbitrator, bessere Verknüpfungsregel), werden versiegelte Epochen nicht still überschrieben: Ein Backfill läuft unter einer neuen code_version, und der Speicher behält die vorherige Berechnung. So hat eine Zahl, die sich ändert, einen sichtbaren Grund — was genau das „unsere Zahlen stimmen nicht überein"-Problem adressiert.

Ingest ist inkrementell. Rohdaten bleiben wie bisher zwischengespeichert (data/raw/), aber die abgeleitete Ebene wird einmal geschrieben und vielfach gelesen. Ein Neustart, ein RPC-Ausfall oder ein Explorer, der die API hämmert, lesen alle aus dem Speicher; nur die laufende Epoche berührt das Netzwerk.

Auswirkung auf die API. /v1/report/{epoch} wird zu einem Speicher-Lookup und kann jede historische Epoche ausliefern, nicht nur den zuletzt gebauten Snapshot. Antworten führen status (sealed / partial / live), computed_at und code_version mit. Die statischen api/sample/*.json bleiben nur noch als Kaltstart-Fallback für eine frische Installation mit leerer Datenbank.


5.1.1 Zwei Takte: was live ist und was nicht

Am Live-Netz gemessen: Der Tick läuft mit ~1,6/s, während sich Computor-Liste, Revenue und Clustering einmal pro Epoche ändern — einmal pro Woche, von Mittwoch 12:00 UTC bis Mittwoch 12:00 UTC (die Tickzahl je Epoche schwankt, 1,08–2,29 Mio. Ticks; die Woche nicht). Die Analyse minütlich abzufragen würde RPC-Budget verbrennen, um eine Woche lang dieselbe Antwort zu erzeugen.

Der Dienst läuft deshalb in zwei Takten:

  • /v1/pulse — Tick, Epochen-Fortschritt, Tick-Qualität, aktive Adressen. Günstig, ~10 s gecacht, ausgelegt auf Abruf alle 15 s. Das ist es, was das Dashboard animiert.
  • Der Report — nur neu berechnet, wenn die Epoche wechselt (das Dashboard beobachtet die Epochennummer des Pulses, statt den Report auf Verdacht zu pollen).

Die laufende Epoche hat außerdem noch kein Revenue — es wird erst bei Epochenschluss gutgeschrieben. Der Hauptbericht ist deshalb die neueste abgeschlossene Epoche, die laufende erscheint separat im Live-Panel. Die laufende als Report zu zeigen hieße, einen leeren Bericht zu zeigen, während ein vollständiger direkt dahinter liegt.


5.2 Gebaut für ein Netz in Bewegung

Das Netz steht nicht still: Epochen kommen laufend hinzu, Pools entstehen, verschmelzen und verschwinden, die Analyse-Ebene bekommt neue Evidenztypen — und selbst „676 Computors" ist eine aktuelle Konstante, kein Naturgesetz. Alles, was auf die heutige Form festgenagelt ist, wird später still zur Falschaussage. Die Regeln, die das verhindern:

  • Keine Netzkonstante ist hartkodiert. Slot-Zahlen, Epochennummern und Betreiberzahlen werden bei jedem Rendern aus den Daten gelesen. Insbesondere enthalten die UI-Sprachdateien keine Zahlen: Die Strings nutzen Platzhalter ({slots}, {epoch}, {operators}), die aus den Live-Daten gefüllt werden. So sagt eine heute geschriebene Übersetzung auch dann noch die Wahrheit, wenn sich das Netz ändert. Eine Sprachdatei darf nie an die Werte einer Epoche gebunden sein.
  • Neue Betreiber brauchen keine Code-Änderung. Ein Pool, der ins Registry aufgenommen wird, oder ein Cluster, den der Payout-Graph neu offenlegt, erscheint einfach im nächsten Epochen-Report — das Clustering ist datengetrieben, nie eine fest gepflegte Liste.
  • Unbekannte Werte degradieren ehrlich. Eine Konfidenzstufe, die diese Dashboard-Version noch nie gesehen hat, wird neutral unter ihrem eigenen Namen dargestellt, nie still als eine bestehende Stufe umetikettiert, und sie wird nicht als on-chain verknüpft gezählt. Im Zweifel „wir können das nicht bestätigen" ist die sichere Richtung.
  • Alte Leser, neue Daten. Die Report-Payloads wachsen additiv: Konsumenten ignorieren unbekannte Felder, ein Explorer mit älterem Embed funktioniert also weiter.
  • Historie ist im Transport begrenzt, nicht auf der Platte. Der Speicher behält jede Epoche für immer, aber die Standard-API-Sichten liefern ein aktuelles Fenster (52 Epochen für die Zeitreihe, 26 fürs Dashboard-Bundle), damit die Antwortgröße konstant bleibt, während die Historie wächst. Der volle Bereich bleibt auf Anfrage verfügbar (?epochs=).
  • Zukunftsfestigkeit wird getestet, nicht angenommen. tests/test_forward_compat.py führt die Analyse mit 100 / 676 / 1 000 / 2 048 Slots aus, über einen wachsenden Epochenbereich, mit mitten in der Historie hinzukommenden Pools und einer injizierten unbekannten Konfidenzstufe.

6. Dashboard & Animation

Das Referenz-Dashboard rendert den Report und — dort, wo es wirklich etwas zu animieren gibt — animiert es. Animation wird nur eingesetzt, wo Bewegung Bedeutung trägt, nicht als Deko:

  • Cluster-Entwicklung über die Epochen — eine animierte Treemap / Bubble-Chart, in der jede Bubble ein Betreiber ist, Größe = Slots oder Revenue; man spielt die Epochen ab und sieht Cluster wachsen, schrumpfen, verschmelzen oder sich aufteilen. Macht sichtbar, wie „ein Pool auf die Top-451-Schwelle zukriecht".
  • Nakamoto-Koeffizient-Timeline — eine animierte Linie über die Epochen, mit markierten Gefahrenschwellen (⅓, ½).
  • Fund-Flow-/Verknüpfungsgraph — ein Force-Directed-Graph aus Slots und Auszahlungs-Verbindungen, der sich zu Clustern einpendelt, sodass die On-chain-Verknüpfung buchstäblich beobachtbar wird.
  • Deklariert vs. erkannt — ein Übergang, der das „offizielle" self-reported Clustering in das erkannte morpht, sodass die Lücke („der Rauch") selbst die Animation ist.

Statische Fallbacks sind immer verfügbar (jede animierte Ansicht hat ein Standbild), weil die Explorer auch die statische Form einbinden können.

6.1 Dashboard-UX-Anforderungen

  • Layout / visuelle Sprache: Orientierung an bestehenden Qubic-Community-Front-Ends (z. B. Qubic Dividends), damit sich der Report nativ im Ökosystem anfühlt — gleiche Karten-/Tabellen-/Dark-Ästhetik, sodass ein Explorer ihn ohne stilistischen Bruch einbinden kann.
  • Dark- / Light-Mode: ein Umschalter im Header. Dark ist Standard (passt zum Ökosystem). Voll theme-aware — beide Modi sind gleichwertig, nicht nachträglich.
  • Sprachumschaltung DE / EN: alle UI-Texte laufen über eine i18n-Schicht mit Deutsch und Englisch. Englisch ist Standard; Deutsch ist eine vollwertige, gleichrangige Übersetzung. So strukturiert, dass später weitere Sprachen ergänzt werden können (einfache Key → String-Dictionaries).
  • Persistenz: sowohl die Theme- als auch die Sprachwahl werden in localStorage gespeichert und beim nächsten Besuch wiederhergestellt. Lese-/Schreibzugriffe sind in try/catch gekapselt, damit die Seite auch korrekt rendert, wenn Storage nicht verfügbar ist (privater Modus, blockierte Cookies) — dann greifen die Defaults (EN + Dark).

7. Warum das die Aufgabe erfüllt

  • Liefert genau die zwei von CFB genannten Dinge: Revenue-Metriken + Dynamik und Clustering mit Computor-Anzahl pro Cluster.
  • Baut auf Self-Reporting als primärer Ebene — der Anti-Sybil-Technik, die CFB „to the fullest" nutzen will — und ergänzt On-chain-/Verhaltens-Ebenen, um die Lücke zu quantifizieren.
  • Wird als API zum Einbinden für Explorer ausgeliefert, was seine erklärte Distributionsanforderung ist.
  • Neutral und reproduzierbar — misst Konzentration, klagt nicht an; jeder kann es nachrechnen, was das Problem des „fehlenden neutralen Ermittlers" beantwortet.

7.1 Community-Feedback und was sich geändert hat (v0.2)

Vier Rückmeldungen zum Entwurf v0.1 und wie das Konzept darauf antwortet. Die ersten drei zeigten auf dieselbe Schwachstelle: eine solide Metrik-Ebene auf einer ungesicherten Datenebene. Die vierte zeigt auf eine andere: einen Report, der immer nur eine Epoche auf einmal betrachtet.

1. „Meine Zahlen stimmen nicht mit deinen überein — ich habe seit etwa 6 Epochen vollständiges Transaction-Tracking laufen." (Kevarms)

Zutreffend — und die Untersuchung förderte etwas Größeres zutage als einen Paginierungsfehler.

v0.1 leitete Revenue aus einem einzigen, nicht paginierten Durchlauf über die Transfers des Arbitrators ab, ohne Epochen-/Tick-Eingrenzung und mit unverifizierter Arbitrator-Identität. §4.3 spezifiziert die Korrektur. Gegen die Live-RPC ausgeführt (07.09.2026) lieferte sie jedoch null — und der Grund ist grundsätzlicher Natur (vollständige Belege in docs/DATA_SOURCES.md §6):

  • Die mitgeführte Arbitrator-Identität bezahlte 0 von 676 Computors — sie war schlicht falsch;
  • Ein Scan von ~4 500 Transaktionen pro Computor über das gesamte Epoche-228-Fenster fand überhaupt keine eingehenden Zahlungen. Was Computor-Identitäten tatsächlich aussenden, ist ein Strom von amount = 0-Transaktionen an die Nulladresse — das sind Solution-Submissions, keine Auszahlungen;
  • Dennoch meldet /v1/balances 0,5–1,6 Mrd. QU eingehenden Wert pro Computor. Der Wert ist real, aber die Transfers, die ihn tragen, werden von den Transaktions-Endpunkten nicht ausgeliefert.

Fazit: Computor-Revenue wird durch Protokoll-Emission gutgeschrieben, nicht durch einen Transfer, den die öffentliche RPC sichtbar macht. „Arbitrator-Auszahlungen verfolgen" kann also nicht funktionieren, so vollständig man auch paginiert — die Datensätze existieren dort nicht. Das ist mit hoher Wahrscheinlichkeit die Wurzel der Abweichung, und es bedeutet: Keine der beiden Implementierungen lässt sich gegen die andere prüfen, solange nicht beide offenlegen, welche Quelle sie verwenden.

Was wir stattdessen tun (§4.3, implementiert): Revenue wird als Differenz des kumulativen incomingAmount jedes Computors über die Epochengrenze gemessen, auf Basis von Balance-Snapshots, die wir selbst nehmen — genau wofür der Speicher (§5.1) existiert. Gegen die Live-Chain verifiziert. Das ist in sich geschlossen, reproduzierbar und liefert ab der nächsten Epochengrenze korrekte Zahlen.

Zwei Dinge bleiben zu tun, das zweite direkt mit Kevarms:

  1. Ein qubic.li-Score-API-Token anfragen — der schnellste Weg zu historischem Revenue und eine unabhängige Gegenprobe.
  2. Methoden vergleichen, nicht nur Zahlen. Da die öffentlichen Transfer-Endpunkte diese Zahlungen nicht führen, muss ein konvergierender 6-Epochen-Datensatz aus einer anderen Quelle stammen (Node-Feed, Pool-API oder Balance-Deltas wie jetzt bei uns). Das zu klären ist wertvoller als ein Streit über Summen — und es ist der eigentliche Abnahmetest.

Aus derselben Session kamen zusätzlich drei konkrete Endpoint-Korrekturen (falsche Pfade, ein Paginierungslimit von 250 Zeilen und wo die Epochen-Tick-Fenster tatsächlich liegen); alle sind in docs/DATA_SOURCES.md §6.1 festgehalten, damit der nächste Implementierer sie nicht wiederholt.

2. „Gutes Tooling, grober Input; On-chain-Verknüpfung sollte der Standard sein, nicht das Auffangbecken namens ‚unattributed'." (Jure Ursic Cergol)

Als architektonische Korrektur angenommen. In v0.1 existierte apply_onchain_linkage(), wurde aber vom Report-Pfad nie aufgerufen — die einzige tatsächliche Zuordnung kam aus einem Registry, dessen computors-Listen bewusst leer sind. Damit wurde jeder der 676 Slots zu einem eigenen „unattributed"-Einzelcluster, und die Kennzahlen beschrieben 676 fiktive unabhängige Betreiber. §4.2 dreht die Schichtung um: Der Payout-Graph wird in jeder Epoche berechnet und liefert das Basis-Clustering; Self-Reporting beschriftet und ergänzt es, kann aber nie trennen, was die Chain verknüpft hat. „Unattributed" heißt jetzt das Ledger zeigt keine Verknüpfung, nicht niemand hat ein Formular eingereicht — und der Report weist seine Verknüpfungs-Abdeckung aus, damit erkennbar ist, wie viel des Netzwerks aufgelöst und wie viel nur angenommen ist.

3. „Musst du nichts persistieren?" (Admin)

Doch. v0.1 baute jede Antwort aus der RPC neu und überschrieb bei jedem Build seine einzigen Snapshot-Dateien — Historie ging verloren und keine zwei Läufe waren vergleichbar. §5.1 führt einen SQLite-Speicher mit genau der Trennung ein, die das Projekt braucht: die aktuelle Epoche wird immer live neu berechnet, damit die laufende Epoche nie veraltet, während abgeschlossene Epochen versiegelt und aus dem Speicher ausgeliefert werden — Historie ist damit unveränderlich, archivierbar und nicht mehr davon abhängig, dass die RPC alte Epochen noch ausliefert. Eine Logikänderung löst einen versionierten Backfill aus statt eines stillen Überschreibens — ändert sich also eine Zahl, gibt es dafür einen festgehaltenen Grund.

4. „Apool macht dicht — wohin gehen die IDs, die sie benutzt haben?" (Vaintor)

Das Konzept hatte darauf keine Antwort, denn jede Schicht in §4.2 argumentiert innerhalb einer einzelnen Epoche. Eine Pool-Schließung ist genau das Ereignis, das ein Per-Epochen-Report nicht deuten kann: Die Slots des abgehenden Betreibers werden neu vergeben, und ob sie bei neuen unabhängigen Betreibern landen, von den verbliebenen großen Pools aufgesogen werden oder unter frischen Identitäten desselben Betreibers zurückkommen — jeder dieser Fälle ergibt eine in sich stimmige Momentaufnahme. Schlimmer noch: Der dritte Fall lässt die Kennzahlen besser aussehen, weil die Nachfolge-Slots als unverknüpfte Einzelstücke eingehen und die Betreiberzahl aufblähen. Der Report hätte das Netzwerk also genau dann beschönigt, wenn er hätte warnen müssen.

§4.4 ergänzt die fehlende Dimension: einen Epochen-zu-Epochen-Identitäts-Diff (retained/departed/entered/succeeded) als erstklassiges Ergebnis, eine Nachfolger-Erkennung, die den bestehenden Payout-Graph nutzt, um die Betreiber-Identität über einen ID-Wechsel hinwegzutragen, eine veröffentlichte Churn-Metrik mit explizit unerklärtem Anteil sowie ein optionales status-Feld in der Registry, damit eine angekündigte Schließung vor der Grenze aktenkundig ist statt im Nachhinein rekonstruiert zu werden. Die Frage ist zugleich ein konkreter Abnahmetest: Wenn Apools Abgangsepoche schließt, muss der Report sagen können, wohin diese Slots gegangen sind — und klar benennen, welchen Teil der Bewegung er nicht erklären konnte.


8. Wo der Report steht

Zwei der drei Ebenen sind fertig und laufen auf Live-Daten. Die dritte — die Frage, wer einen Slot betreibt — ist die, die die Chain nicht beantworten kann, und an dieser Stelle hängt das Projekt jetzt an der Community und nicht an weiterem Code.

Funktioniert, auf Live-Daten:

  • Umsatz pro Computor-Slot, aus dem End-Epoch-Log eines Bob-Nodes. Die Epochen 225–228 sind versiegelt, mit je 676/676 bezahlten Computors. (§4.3)
  • Konzentration und ihre Dynamik: Gini, HHI, Nakamoto ⅓/½, Top-N-Anteil, pro Epoche und als Zeitreihe. (§3)
  • Ein Store, der sich selbst aktuell hält: Historie beim ersten Start nachgefüllt, jede Epoche beim Abschluss versiegelt, und jede Epoche, die eine ältere Code-Version berechnet hat, wird beim Deploy neu abgeleitet — eine Korrektur zieht die Zahlen zurück, die sie behebt. (§5.1)
  • API und Dashboard zeigen nie eine Zahl, die niemand gemessen hat: kein Sample-Fallback, ein ungefüllter Store antwortet mit 503, und jede Zahl ist danach beschriftet, was sie zählt.

Offen — und das ist der Befund, kein Mangel:

Das Clustering läuft in jeder Epoche und ergibt derzeit einen Cluster: 676 nicht zugeordnete Slots, weil keine der beiden Attributionsebenen etwas auflöst.

  • On-Chain-Verknüpfung (§4.2) ist implementiert und findet nichts Beweisbares. Jeder Computor wird von derselben Null-Adresse vergütet — Protokoll-Emission, keine Wallet — und seine ausgehenden Transfers sind einheitlich 1.000.000-QU-Burns an dieselbe Adresse. Kein Transfer-Graph verbindet zwei Computors. Ausgewiesen als linkage_coverage: 0 statt als Unabhängigkeit dargestellt.
  • Self-Reporting (§4.1), CFBs eigener Anti-Sybil-Punkt, ist leer: kein Pool hat seine Slots deklariert. Registry, Schema und Validator stehen bereit und warten.

Der Report misst daher Umsatz pro Slot und beschriftet das durchgehend so: Ein Nakamoto-Koeffizient von 222 bedeutet 222 von 676 Slots, nicht 222 unabhängige Betreiber. Hält ein Betreiber mehrere Slots, ist die echte Konzentration höher — und die Seite sagt das.

Das ist eine Messung für sich — die Self-Reporting-Beteiligung liegt derzeit bei null — und das Werkzeug ist die Infrastruktur, um das zu ändern. Ein Pool öffnet einen Pull Request auf data/self_reporting/pools.json; die Betreiber-Ansicht (Treemap, Betreiber-Tabelle, Nakamoto auf Betreiber-Ebene) schaltet sich für diesen Pool ohne Code-Änderung ein, und die Git-Historie ist der Prüfpfad. Das ist „Self-Reporting to the fullest" konkret gemacht.


Generiert aus docs/CONCEPT.de.md — bearbeite das Konzept, nicht diese Seite. Quelldokument auf GitHub lesen