The quote describes DAO v1.1, not the full CKB story.
In DAO v1.1, the latest vote, the latest binding, and the DAO weight at the snapshot must be reconstructed from history rather than read from live cells, so the tally remains off-chain.
The proposed Deposit-Paired Voting design keeps decisive state in live cells and validates it during state transitions.
Summary
- Off-chain tally: works when the operator role and recomputation path are explicit.
- Recomputation: independent off-chain recomputation is not the same property as self-contained on-chain tally.
- DAO v1.1: improves auditability over DAO v1.0, but does not support self-contained on-chain tally because decisive state is still reconstructed off-chain.
- Missing proofs:
latest-state completeness(proving no later vote or binding superseded the counted one) and snapshot-time DAO liveness, not arithmetic. - Requirement: self-contained on-chain tally needs proposal state, voter control state, and deposit binding to live in cells that protocol rules validate.
- Proposal: Deposit-Paired Voting keeps proposal state, controller state, and deposit binding in
VoteMeta,VotingRightOwner, andVotingRightOwned. It derives a time-independent, proposal-independent protocol weight from the backing deposit incell_deps, the deposit’s creation header, and fixed genesis accumulated rateAR_0. - Costs: one proposal-wide serialized update lane, one per-voting-right serialized update lane, parked-deposit custody, and a one-pending-vote confirmation model.
What Self-Contained On-Chain Tally Requires
Self-contained on-chain tally on CKB requires a small set of properties:
- authoritative proposal state in live cells validated by protocol rules
- authoritative voter control state in live cells validated by protocol rules
- an explicit, locally verifiable binding between voting power and the backing asset in the current transaction
- voting weight derived from a script-verifiable deposit, its creation header, and fixed genesis
AR_0, rather than reconstructed from raw DAO liveness at vote end - delegation, if retained, validated by current transitions rather than reconstructed later
- finalization rules that use only inputs, deps, headers, and
sincerelations that CKB can actually verify throughheader_deps,ckb_load_header, and the since verification rule
In every case, authoritative state must live where CKB validation can read it. CKB can read specific inputs, cell_deps, headers named in header_deps, and since locks. It cannot read the full live cell set from a block header alone.
How DAO v1.1 Produces a Result
DAO v1.1 reaches a result by rebuilding state off-chain and then summing it.
The result path in build_vote_results does five things:
- Loads latest vote rows from
all_votes. Before that step, the vote indexer has already reduced history to one latest row per address throughquery_all_votesandquery_vote_records_by_epoch_opt. - Applies the duplicate-row guard, which invalidates any
ckbAddressthat appears more than once in theall_votesresponse. - Expands bindings and stake through
get_weight. - Applies the bound-address override rule through the
self_weight_addr_setfilter. - Sums the resulting weights into per-candidate totals.
That tally depends on five separate claims:
- The vote indexer supplied the correct latest vote rows.
- The binding indexer supplied the correct latest binding state before vote end.
- The DAO indexer supplied the correct live deposit weights at the snapshot height.
- The bound-address override rule was applied correctly.
- The arithmetic is correct.
Only claim 5 is a direct arithmetic check once the counted state is fixed. The other four are reconstructed-state claims about which state may be counted.
Why DAO v1.1 Still Depends on Off-Chain Reconstruction
DAO v1.1 cannot move tally on-chain because it cannot prove final vote state, current binding state, and snapshot-time DAO liveness.
1. Vote changing turns tally into a latest-state claim
Once votes and weights can change, tally requires proof that no later change superseded the counted state before vote end.
The DAO v1.1 vote-changing requirements explicitly allow vote changing, vote cancellation, and dynamic weights. The verified tally path uses query_vote_records_by_epoch_opt, get_weight, and query_dao_stake_until_height to reduce votes to latest indexed rows and apply weight snapshots at vote end.
The vote indexer makes that latest-state choice: it scans vote outputs in vote mode, vote_output_handle inserts them into vote_record, and query_vote_records_by_epoch_opt reduces them to one latest row per address. That remains an off-chain completeness decision.
2. Existing vote cells do not provide stable tally inputs
Current vote cells are not stable tally inputs. They are built for vote submission, not later self-contained aggregation.
Current vote-building clients create vote cells with lock: voteAddr.script in both the standalone user-vote transaction builder and the frontend voting transaction builder. The contract separately checks that VoteProof.lock_script_hash matches an input lock hash.
After the fact, an on-chain tally has only two options:
- consume vote cells as inputs: every voter must sign the tally transaction
- reference vote cells through
cell_deps: the tally transaction needs no voter signatures, but every referenced dep must still be live at validation
The second option races with ordinary spending. If the tally builder selects vote cell V as a cell_dep and the voter spends V first, the tally transaction fails because resolve_transaction rejects dead outpoints while resolving deps.
Historical proof can show the vote transaction and its witnesses against a block header, but not the previous output lock scripts. Raw transaction inputs contain only previous_output and since, not those lock scripts. A TX-in-witness approach that keeps the current rule also needs proofs for the referenced input cells and their lock scripts.
3. Binding state is reconstructed off-chain
Binding state is also reconstructed from indexed history, not read from live protocol state. The address-bind indexer reads witness-carried bind data in verify_tx, stores it in bind_info while blocks are scanned, and answers queries through query_by_to_at_height.
An on-chain tally can derive “latest binding before vote end” only if binding becomes live state validated by protocol rules, or if the tally accepts an external completeness assumption.
4. DAO weight at vote end depends on snapshot-time liveness
Snapshot-time DAO liveness also remains off-chain, because CKB headers do not commit to the live cell set.
The CKB RawHeader schema includes transactions_root, proposals_hash, extra_hash, and dao, but no Ethereum-style state root. The transactions_root calculation commits to merkle_root([raw_transactions_root, witnesses_root]), not the live cell set.
A header alone cannot prove that a deposit cell was still live at snapshot block H. That is enough for audit-oriented recomputation, but not for a self-contained on-chain tally of raw DAO-backed weight.
What DAO v1.1 Publicly Claims
DAO v1.1 publicly claims independent off-chain recomputation, not self-contained on-chain tally.
Docs: DAO v1.1’s public docs frame the target as independently verifiable results. The same docs define address weight in terms of Nervos DAO stake, take the voter list source from DAO stakers, and later filter that list to DID users. That framing matches independently recomputable off-chain tally, not self-contained on-chain tally.
Forum claim: The public claim that anyone can recompute the tally without a centralized API still describes off-chain recomputation, not self-contained on-chain tally. Recomputing the result still requires rebuilding the same off-chain latest-state pipeline: latest vote rows from query_vote_records_by_epoch_opt, latest binding state from query_by_to_at_height, and DAO stake snapshots from query_dao_stake_until_height.
Implementation boundary: The implementation also requires proposal-time latest VoterList selection, VoteMeta SMT-root construction in build_vote_meta, and backend proof issuance through get_proof and prepare before a user can submit a vote. Those steps still shape who can vote and what proof the vote carries.
Deposit-Paired Voting Proposal
Deposit-Paired Voting is a proposed design that moves decisive state into live cells without minting a separate voting token.
State Model
The proposal keeps state in three live cell types:
VoteMeta: a proposal cellVotingRightOwner: a user-owned controller cellVotingRightOwned: a protocol-locked backing DAO deposit cell
A VotingRightOwner and VotingRightOwned together form one voting-right pair.
VoteMetastores the confirmed tally, one pending vote slot, the deadline, theopenorfinalstate, and proposal identity after the firstvoteorcloseVotingRightOwnerstores owner authority, optional delegate authority, the paired backing outpoint, and per-proposal entriesVotingRightOwnedholds the parked DAO deposit and enforces custody only
Ordinary vote updates consume and recreate VoteMeta and VotingRightOwner. They do not move the backing deposit.
Proposal id: Each proposal needs a stable id that survives VoteMeta recreation. The initial VoteMeta stores no proposal id. The first vote or close transaction reads the consumed input’s previous_output, copies that creation OutPoint into the recreated VoteMeta, and later updates preserve it.
Custody: VotingRightOwned enforces only custody invariants: the backing deposit stays paired to one controller, cannot be redirected, and can be released only through the paired release path.
Initialization and Binding
Voting-right creation must establish the pair once and keep it stable afterward.
Voting-right creation uses one transaction:
- create
VotingRightOwnedas a fresh DAO deposit under Deposit and lock it with the voting-right logic - create
VotingRightOwnerin the same transaction and pair it with that deposit
Existing deposits: If the user starts from an existing user-controlled DAO deposit, it must go through the normal Withdraw Phase 1 path and then be redeposited under the voting-right lock. Deposited input handling shows that a deposited DAO input cannot be re-locked directly into another deposited cell.
Pair binding: The creation transaction can link the pair by output position. Later vote and revote updates bind it by the committed OutPoint, so the backing deposit cannot be swapped behind the controller.
Proposal creation: A proposal starts as a fresh VoteMeta with no stored proposal id, a deadline, zero confirmed tally, an empty pending vote slot, and open state.
Authority and Release
Each cell validates the invariant it owns:
VoteMetaowns proposal identity, tally updates, pending-vote confirmation, and theopen -> finaltransition- the voting-right logic owns correct pairing of
VotingRightOwnerandVotingRightOwned, weight derivation from the parked deposit and its creation header againstAR_0, and custody of the parked deposit - the owner or delegate path owns vote and revote updates to per-proposal entries and delegated voting authority
Delegation: A delegate may cast and revote on behalf of the owner, but cannot redirect the backing deposit, release the voting right, or perform unrelated state changes.
Release: VotingRightOwner and VotingRightOwned are consumed together. Release succeeds only when every proposal entry recorded in VotingRightOwner is already past its deadline under the transaction’s encoded since threshold. CKB enforces that threshold under the since verification rule and verify_absolute_lock. Release does not depend on a third party closing every touched proposal. It depends only on whether all proposals previously touched by that voting right are past their deadlines.
Derived Weight and Verifiable Timing
The design derives weight and proves timing without snapshot-time DAO liveness.
Weight rule: Weight is neither the current withdrawable CKB amount at vote end nor a value stored in VotingRightOwner. Each calculation derives it from the backing deposit in cell_deps and its creation header. The DAO accumulated-rate formula defines AR_i and genesis AR_0 : 10 ^ 16, and CKB’s maximum withdraw formula uses the same ratio. With genesis normalization, the sketch is:
weight(counted_capacity, AR_m) {
return counted_capacity * AR_0 / AR_m
}
Consequence: The protocol weight does not vary with later time or with the proposal being voted on. That said, User Interface can still show the deposit’s current withdrawable amount.
Runtime loading: The creation transaction can pair VotingRightOwner with VotingRightOwned, but it cannot compute voting weight because it cannot load its own inclusion header. CKB scripts read only explicit header_deps. They load creation headers through ckb_load_header from referenced inputs or cell_deps. A later vote, revote, or close transition must include the backing deposit in cell_deps, load its creation header, and derive weight there against fixed genesis AR_0.
Because a vote transaction cannot prove its own deadline position at submission time, VoteMeta carries the confirmed tally plus one pending vote slot. The pending slot stores the VotingRightOwner identity and the pending choice.
Vote and revote: vote and revote consume VoteMeta and VotingRightOwner. They keep VotingRightOwned parked, verify the pair through the backing deposit in cell_deps, and derive weight from the deposit’s creation header against AR_0. If the consumed VoteMeta was created before the deadline, the previous pending vote is confirmed into the tally. If it was created after the deadline, that pending vote is discarded.
Close: close consumes VoteMeta, proves finalization timing under the since verification rule and verify_absolute_lock, reloads the pending VotingRightOwner and backing deposit from cell_deps, derives the last pending vote’s weight against AR_0, and moves the proposal from open to final.
The one-pending-vote model follows directly from what a vote transaction can and cannot prove about its own inclusion time.
Why It Fits the On-Chain Tally Requirements
Against the requirements above, the proposal stores proposal state, controller state, and deposit binding in live cells, then validates timing and weight during each transition.
Compared with DAO v1.1:
| Dimension | DAO v1.1 | Deposit-Paired Voting |
|---|---|---|
| Vote state | Reconstructs latest vote state off-chain | Stores current vote state in live cells |
| Binding state | Reconstructs latest binding state off-chain | Stores an explicit controller-to-deposit pair in live cells |
| Weight derivation | Needs raw DAO snapshot liveness at vote end | Derives the same protocol weight from the backing deposit and its creation header against AR_0 in each transition that needs it |
| Result assembly | Relies on an off-chain tally job to assemble final state | Updates tally state inline in VoteMeta |
| Delegation and binding | Treats delegation and binding as off-chain interpretation | Validates both during current transitions |
The arithmetic rule stays the same. Deposit-Paired Voting moves authoritative state into live cells.
Tradeoffs and Limits
Deposit-Paired Voting still makes some Tradeoffs:
- one proposal-wide serialized update lane through
VoteMeta - one per-voting-right serialized update lane through each
VotingRightOwner - the backing DAO deposit stays parked in
VotingRightOwnedwhile the voting right exists - a pending vote becomes confirmed only when a later vote or
closeresolves it
Conclusion
The limit is not CKB: tally follows the deciding state.
In DAO v1.1, that means off-chain tally, plus all the off-chain complexity that comes with it.
Deposit-Paired Voting is a proposed design that flips that setup. Put the deciding state in live cells. Then the tally can live there too: no separate voters whitelist, no off-chain reconstruction stack to run, no snapshot-time DAO liveness requirement.
Such is the difference between a system independently verifiable and one that lives on-chain.

