Deposit-Paired Voting

This proposes a deposit-paired voting core for CKB. It keeps voting weight tied to real DAO deposits while moving the tally into live-cell state.

Community DAO v1 is the motivating profile, not a consensus commitment to any forum, website, or governance operator.

This thread follows the earlier on-chain tally discussion and focuses only on the design. For Community DAO v1 phase-2 approval votes, it can replace the authoritative tally path without making proposals official, enforcing publication policy, or executing phase-3 disbursement.

That buys on-chain tally correctness, but only with explicit custody, contention, and migration tradeoffs.

Lifecycle

pair_create creates the voting right, proposal_create creates the proposal, and vote or revote stages choices. close seals a tally when needed. After all touched deadlines pass, release_to_withdraw starts normal DAO withdrawal. Optional delegation and intent cells stay outside this base lifecycle.

Base Constraints

  • Each voting right is one pair: one VotingRightOwner controller plus one parked VotingRightOwned Nervos DAO deposit. No separate voting token is minted.
  • Voting weight is normalized DAO weight derived from the backing deposit’s counted capacity and creation-time accumulated rate, scaled to fixed genesis AR_0.
  • ProposalDocument can record governance-profile metadata. Consensus does not enforce discussion-stage, quorum, or pass-threshold rules, but any deposit-backed amount it records must be validated from live proof deposits.
  • Voting means choosing one label index from the ordered label set committed in ProposalDocument.
  • Release starts standard Nervos DAO withdraw phase 1. Final free transfer happens later under normal phase 2 rules.

Non-Goals

  • Officialness stays off-chain: consensus creates valid proposal instances, while publication attestation and governance-profile policy decide which proposal instance counts as official.
  • Fair inclusion stays out of scope: the protocol enforces vote-state correctness on a hot VoteMeta cell but does not provide first-seen or fair-order inclusion guarantees.
  • Voting weight is time-independent: the authoritative tally unit is normalized protocol weight, so displayed DAO balance and voting weight can diverge.
  • This is not a drop-in upgrade for existing DAO deposits: users must migrate deposits under voting-right custody, and adopting the design as the official DAO path would itself require a later meta-rule vote.

Open question: is on-chain tally correctness the right base-protocol boundary?

3 Likes

Proposal Identity, Proposal Documents, And Officialness

Proposal Identity

Each proposal voting instance is a VoteMeta metapoint. The canonical proposal id is the outpoint of the initial VoteMeta cell.

The first vote or close materializes meta_point_id from the consumed VoteMeta input’s previous_output; later transitions preserve it. This keeps identity anchored to the initial VoteMeta outpoint instead of moving with each state update.

Proposal Document

The proposal source is the ProposalDocument witness aligned with the initial VoteMeta output in proposal_create. The proposal id is the initial VoteMeta outpoint, not a document hash, so different proposal instances may reuse identical document bytes.

ProposalDocument lives in WitnessArgs.output_type at the witness index matching the created VoteMeta output. proposal_create fixes that output to index inputs.len().

Clients that need exact proposal bytes must fetch the proved proposal_create transaction and read the witness aligned with proposal_id.index; the proposal id is only a locator because the transaction hash excludes witnesses.

Witness authentication requires raw transaction and witness data at the same block transaction index, proved against the header’s transactions_root, which commits both raw_transactions_root and witnesses_root.

Some proposer lock families may also sign the aligned ProposalDocument witness, but proposer locks are owner-selected, so this is not a base consensus rule.

Proposal Document Shape

ProposalDocument owns the human-readable text, ordered labels, VotingRule, discussion reference, optional governance-profile metadata, and optional budget fields. Live VoteMeta owns only the deadline, tally, pending action, status, and meta_point_id.

Consensus commits VotingRule but does not interpret quorum, thresholds, winner selection, tie handling, or execution policy. Clients should reject rules they cannot validate, and rules should not duplicate labels, label count, or deadline because those already have authoritative sources.

For budget requests, recipient_lock remains document-only; phase-3 operators fetch the proved ProposalDocument, recompute the recipient lock hash from that witness, and apply execution policy outside consensus.

Proposal Metadata And Policy

proposal_create does not require proposer-weight eligibility. It can create a proposal with no proposer deposit, which keeps the base path usable outside Community DAO v1.

For Community DAO v1-style proposals, ProposalDocument can record profile metadata: discussion-stage reference, validated proposer-deposit amount, budget amount, proposal class, quorum rule, and pass threshold. If the document records an available-deposit amount, proposal_create must validate that amount from supplied live proof deposits and creation headers. Clients and governance profiles decide whether that proposal instance is valid for their context.

Publication And Officialness

A proposal is chain-valid after successful proposal_create; it is official only if a recognized off-chain source publishes that exact proposal_id and the active governance profile accepts it. Consensus owns proposal identity and tally sealing; clients and operators interpret the sealed tally through ProposalDocument, publication attestations, and governance-profile policy.

Open question: should consensus validate deposit-backed metadata while leaving discussion-stage, proposer eligibility, quorum, thresholds, publication, and execution policy to clients?

1 Like

Voting Rights, Parked DAO Deposits, And Normalized Weight

Base Authority

Base voting rights are owner-only: vote, revote, and release_to_withdraw require the owner path.

Cell Model

The three cells are VoteMeta for tally state, VotingRightOwner for owner-controlled state, and VotingRightOwned for the parked Nervos DAO deposit.

Owned Lock Modes

VotingRightOwnedLock has parked and release modes. Parked mode only allows paired release_to_withdraw: one matching owner input, no recreated owner output, matched owned_ref, and a same-index DAO withdrawing output under release mode.

Release mode only controls final_unlock for the withdrawing cell. It is permissionless after release_to_withdraw, and enforces that all non-fee withdrawal value goes to the committed final unlock target and that the fee does not exceed the committed max final unlock fee.

Owner State

VotingRightOwnerState stores owned_ref plus one entry per proposal touched by this voting right. Each entry stores proposal id, deadline, and current choice.

owned_ref starts as a sibling output index and materializes to the full owned outpoint on the first owner update. Entries stay until release, preventing delete-and-reenter but making owner updates grow with active proposal count.

Pair Creation

pair_create creates exactly one VotingRightOwner and one paired DAO deposit; that creation path cannot create extra owner or owned-lock outputs.

Weight Rule

The protocol uses time-independent normalized DAO weight for tally updates. Clients and governance profiles can use the same unit for proposer eligibility, quorum, or threshold rules. It scales each deposit’s counted capacity back to fixed genesis accumulated rate AR_0, so later DAO compensation does not change voting weight.

weight(counted_capacity, deposit_ar) {
    return counted_capacity * AR_0 / deposit_ar
}
  • counted_capacity is the parked deposit’s interest-bearing portion: live cell capacity minus occupied capacity under the fixed VotingRightOwned lock, type, and data shape.
  • deposit_ar is the accumulated rate from the backing deposit’s creation header.

Backing Deposit Proof

Base vote and revote prove the exact paired DAO deposit by including the cell named by the owner’s effective owned_ref as a direct cell_dep at any index. They verify it is still a parked deposit under VotingRightOwnedLock, require its creation header, and derive normalized weight from that deposit. The proof does not require a fixed cell_deps index, so transactions can carry more than one paired deposit proof.

Open question: are the owner/owned custody rules sound, and is normalized DAO weight the right tally unit?

2 Likes

Tally State Machine, Pending Actions, And Timing

The canonical proposal id is the initial VoteMeta outpoint, and VoteMeta.tally[i] maps to ProposalDocument.labels[i].

VoteMeta State

VoteMeta stores meta_point_id, immutable deadline, Open or Closed status, immutable-length tally, and at most one pending action.

VoteMeta requires:

  • tally.len() == ProposalDocument.labels.len()
  • 2 <= ProposalDocument.labels.len()
  • every stored or submitted choice index is less than tally.len()

pending holds at most one staged action. A staged action becomes effective only when a later vote, revote, or close resolves it. close reuses the stored pending weight instead of reloading owner or deposit state.

Proposal Creation

proposal_create creates the initial VoteMeta cell. It requires:

  • no VoteMetaType inputs
  • exactly one VoteMetaType output
  • no other VoteMetaType outputs in the same transaction

The created output stores deadline, status = Open, a zeroed tally vector with one slot per document label, empty pending, and no meta_point_id yet. The canonical metapoint id is materialized on first vote or close.

The witness entry aligned with the created VoteMeta output carries the canonical ProposalDocument. Consensus validates label count, tally length, and later choice-index ranges, but it does not parse application-specific voting-rule semantics such as quorum or pass threshold.

proposal_create cannot prove that deadline equals proposal commit time plus a fixed voting duration because CKB predicates do not expose the current transaction’s eventual commit time.

Vote

vote creates the first owner entry for a proposal and stages a first pending action.

vote requires:

  • one VoteMeta update
  • one owner update
  • owner authorization
  • Open pre-deadline state
  • an in-range choice
  • no existing owner entry for this proposal
  • no same-right pending action
  • paired backing-deposit proof

The transition resolves any existing different-owner pending action, materializes meta_point_id and owned_ref if needed, adds the owner entry, sets the owner entry’s current choice, and stages the new pending action with prior_choice = None.

Revote

revote updates an existing owner entry and stages a replacement choice or retraction.

revote has the same VoteMeta, owner, authorization, and backing-deposit checks as vote. It also requires:

  • an existing owner entry
  • an in-range replacement choice or None for retraction

The transition resolves any existing pending action, stages a new action whose prior_choice equals the matched entry’s current choice, preserves the matched entry’s meta_point_id and deadline, updates that entry’s current choice, materializes owned_ref if needed, and replaces pending.

Close

close resolves the final pending slot and marks the proposal Closed.

The base close path does not require owner inputs, backing-deposit cell_deps, or backing-deposit creation headers, so release of a voting right cannot block final tally sealing.

close requires the consumed VoteMeta input to carry an absolute timestamp-metric since threshold whose decoded millisecond threshold is at least the proposal deadline.

If that input was created before the deadline, close resolves pending into the tally. If it was created at or after the deadline, close discards pending. It then clears pending and sets status = Closed.

Consensus does not interpret quorum, pass threshold, or winning condition. Readers derive those from the proved ProposalDocument and client-supported VotingRule.

Timing Model

The consumed VoteMeta cell’s creation time controls vote and revote; close uses an absolute timestamp-metric since threshold so it cannot commit before CKB’s median-time rule reaches the deadline. Because CKB predicates cannot prove an upper commit-time bound, a post-deadline vote or revote may still spend a pre-deadline VoteMeta; it can only resolve the older staged action, and close will discard the newly staged one.

Open question: does the pending-slot design preserve tally correctness around contention, revotes, retractions, and post-deadline transactions?

2 Likes

Release Path And Liveness Limits

Release To Withdraw

release_to_withdraw dissolves the controller half of the voting-right pair and starts standard Nervos DAO withdraw phase 1.

It requires:

  • one owner input
  • the paired parked DAO deposit input
  • no recreated owner or parked-deposit output
  • all owner-entry deadlines passed by the owner input’s since
  • a same-index DAO withdraw-phase-1 output under release-form VotingRightOwnedLock

CKB consensus applies a same-size lock rule to DAO deposit-to-withdraw pairs. The withdrawing cell stays under a same-size release-form VotingRightOwnedLock, committing the final unlock target lock or lock hash plus max_final_unlock_fee.

Final unlock then follows normal DAO phase 2 under that release-form lock.

release_to_withdraw does not depend on a third party closing every touched proposal because close does not depend on later owner or deposit liveness.

Light-Client Fit

CKB light clients can track script-filterable proposal state and prove transaction inclusion; authenticated retrieval of ProposalDocument witness bytes, officialness, cost, and liveness remain outside that proof surface.

Contention And Inclusion Limits

All tally updates serialize through the hot VoteMeta cell. Tx-pool conflict handling is input-based, with optional RBF behavior, so same-input contention remains a liveness risk.

Tradeoffs

  • Stake remains fragmented; the design does not aggregate voting weight across deposits.
  • Keeping current deposits under existing locks requires an off-chain system; using this on-chain system requires normal DAO withdrawal and a fresh deposit under voting-right logic.

Open question: does the release path let voting rights exit without blocking final tally sealing, given the stated liveness limits?

1 Like

Optional Extension: Delegated Voting

Delegation is not part of the base protocol. If added, it should be a replacement lock on the VotingRightOwner cell.

Delegate authority would cover vote and revote only. Proposer-eligibility metadata, publication policy, and release_to_withdraw would remain outside delegate authority.

The delegation lock occupies the slot that would otherwise hold the user’s owner lock. It has two paths:

  • delegated vote or revote
  • user-controlled vote, revote, release_to_withdraw, and delegation changes

Delegate-Authorized Vote And Revote

Delegate-driven vote and revote use the same state transition as base transactions. The delegation lock runs as the owner-cell lock and authorizes the vote path with delegate credentials.

Set Delegate And Clear Delegate

set_delegate and clear_delegate are owner-authorized lock rewrites on the VotingRightOwner cell. They preserve VotingRightOwnerState.

set_delegate replaces the user owner lock with the delegation lock. clear_delegate restores the user owner lock.

Open question: should delegation stay outside the base design, and if added later, do these boundaries prevent delegated release or policy use?

2 Likes

Optional Extension: Intent-Cell Voting

Optional intent cells are a user-side mitigation for contention friction, outside the base protocol. They must preserve VoteMeta tally semantics, weights, and the final Closed result from the base path.

Intent Cell Goal

Users can submit direct vote or revote transactions and retry on contention, or temporarily move VotingRightOwner into an intent-controlled cell so an aggregator handles inclusion. The hot VoteMeta lane remains; batching vote actions into one VoteMeta transition is disallowed because each counted pending action needs its own consumed VoteMeta creation timestamp.

An aggregator may help with inclusion, but cannot change the target proposal, choice, returned owner-cell lock hash, backing deposit custody, or committed max fill fee.

VoteIntentLockArgs

The intent lock commits:

  • owner-cell lock hash
  • proposal id
  • choice or retraction
  • nonce
  • intent_deadline
  • return owner-cell lock hash
  • max_fill_fee

intent_deadline guides filler and cancel policy; it is not a consensus-enforced upper bound on fill commit time.

Create Vote Intent

create_vote_intent converts an owner-controlled VotingRightOwner into an intent-controlled VotingRightOwner for one proposal and one staged action.

It is an owner-authorized owner-cell lock rewrite. It requires:

  • entries are preserved
  • owned_ref is materialized or preserved
  • cell capacity is preserved
  • return owner-cell lock hash is committed
  • max_fill_fee is committed
  • choice_index = None only when the owner already has an entry for that proposal

Fill Vote Intent

fill_vote_intent consumes one intent-controlled VotingRightOwner cell and the current VoteMeta, then executes the same transition as base vote or revote.

fill_vote_intent requires:

  • exactly one base vote or revote
  • the same backing deposit and headers
  • matching proposal_id and choice_index
  • owner cell returned to return_owner_cell_lock_hash
  • owner cell capacity reduced by at most max_fill_fee

If meta_point_id is materialized it must equal proposal_id; otherwise the consumed VoteMeta input outpoint itself must equal proposal_id.

Cancel Vote Intent

cancel_vote_intent lets the owner reclaim the controller cell from the intent lane without touching VoteMeta.

cancel_vote_intent uses an owner-auth input matching owner_cell_lock_hash, preserves entries and capacity, materializes or preserves owned_ref, and returns the cell to return_owner_cell_lock_hash. No fill fee is taken on cancel.

Open question: should intent cells remain an optional extension rather than part of the base voting path?

2 Likes

Reference Transaction Sketches

These sketches are optional and non-normative. If they conflict with earlier rule comments, the rule comments win.

Tally Proof Surface Summary

Path Proof cells Headers Weight source
proposal_create optional proof deposits for deposit-backed metadata proof deposit creation headers validated document metadata only
vote paired backing deposit VoteMeta creation plus deposit creation paired backing deposit
revote paired backing deposit VoteMeta creation plus deposit creation paired backing deposit
close none in the base path VoteMeta creation stored pending.weight
optional fill_vote_intent paired backing deposit VoteMeta creation plus deposit creation paired backing deposit

For proposal_create, proof deposits and headers are required only when ProposalDocument records deposit-backed amounts.

All script matches below mean full CKB script identity: code_hash, hash_type, and args.

pair_create

CellDeps:
    - VotingRightOwnerType code cell
    - Nervos DAO type script
Inputs:
    - funding cells
Outputs:
    - VotingRightOwner at output index j:
        Lock: user owner-cell lock
        Type: VotingRightOwnerType
        Data:
            owned_ref: CreationOwnedIndex(i)
            entries: []
    - VotingRightOwned deposit at output index i:
        Lock: VotingRightOwnedLock parked mode
        Type: Nervos DAO
        Data: DAO deposit data

The creation path rejects extra VotingRightOwner outputs or VotingRightOwnedLock outputs, and checks that the owner owned_ref points to the single paired DAO deposit output.

proposal_create

CellDeps:
    - optional proof deposits if ProposalDocument records deposit-backed amounts
    - VoteMetaType code cell
HeaderDeps:
    - matching proof deposit creation headers when proof deposits are supplied
Inputs:
    - funding cells
Witnesses:
    - witness at index inputs.len(): WitnessArgs.output_type = ProposalDocument
Outputs:
    - funding/change outputs before index inputs.len()
    - VoteMeta at output index inputs.len():
        Lock: AlwaysSuccess
        Type: VoteMetaType
        Data:
            meta_point_id: None
            deadline: ProposalDocument deadline
            status: Open
            tally: zero vector matching ProposalDocument labels
            pending: None

vote Or revote

CellDeps:
    - direct paired VotingRightOwned dep cell matching effective owner.owned_ref, at any cell_deps index
    - VoteMetaType code cell
    - VotingRightOwnerType code cell
HeaderDeps:
    - consumed VoteMeta creation header
    - paired VotingRightOwned creation header
Inputs:
    - VoteMeta:
        Lock: AlwaysSuccess
        Type: VoteMetaType
        Data: current proposal state
    - VotingRightOwner:
        Lock: owner-cell lock
        Type: VotingRightOwnerType
        Data: effective owned_ref and proposal entries
Outputs:
    - VoteMeta:
        Lock: AlwaysSuccess
        Type: VoteMetaType
        Data: materialized meta_point_id, resolved older pending if any, new pending
    - VotingRightOwner:
        Lock: same owner-cell lock
        Type: VotingRightOwnerType
        Data: materialized owned_ref if needed, owner entry added or updated
        Capacity: same as input VotingRightOwner

close

CellDeps:
    - VoteMetaType code cell
HeaderDeps:
    - consumed VoteMeta creation header
Inputs:
    - VoteMeta with absolute timestamp-metric since threshold:
        Lock: AlwaysSuccess
        Type: VoteMetaType
Outputs:
    - VoteMeta:
        Lock: AlwaysSuccess
        Type: VoteMetaType
        Data: materialized meta_point_id if needed, Closed, pending None, final tally

release_to_withdraw

CellDeps:
    - VotingRightOwnerType code cell
    - VotingRightOwnedLock code cell
    - Nervos DAO type script
HeaderDeps:
    - parked deposit header
Inputs:
    - VotingRightOwned parked DAO deposit at input index i, matching effective owner.owned_ref:
        Lock: VotingRightOwnedLock parked mode
        Type: Nervos DAO
    - VotingRightOwner with absolute timestamp-metric since threshold:
        Lock: owner-cell lock via user path
        Type: VotingRightOwnerType
        Data: all entry deadlines passed
Outputs:
    - DAO withdrawing cell at output index i:
        Lock: VotingRightOwnedLock release mode
        Type: Nervos DAO

final_unlock

CellDeps:
    - VotingRightOwnedLock code cell
    - Nervos DAO type script
HeaderDeps:
    - deposit header
    - withdrawal request header
Inputs:
    - DAO phase-1 withdrawing cell under release-form VotingRightOwnedLock with absolute epoch-number since threshold
Witnesses:
    - input witness: WitnessArgs { input_type: deposit-header index }
Outputs:
    - plain user cells at the committed final unlock target

Optional fill_vote_intent

Each fill_vote_intent still executes one base vote or revote; it is an inclusion helper, not a batching rule.

VoteIntentLockArgs commits owner_cell_lock_hash, proposal_id, choice or retraction, intent_nonce, intent_deadline, return_owner_cell_lock_hash, and max_fill_fee.

CellDeps:
    - direct paired VotingRightOwned dep cell matching effective owner.owned_ref, at any cell_deps index
    - VoteMetaType code cell
    - VotingRightOwnerType code cell
    - VoteIntentLock code cell
HeaderDeps:
    - consumed VoteMeta creation header
    - paired VotingRightOwned creation header
Inputs:
    - VoteMeta:
        Lock: AlwaysSuccess
        Type: VoteMetaType
        Data: current proposal state
    - VotingRightOwner intent cell:
        Lock: VoteIntentLock
        Type: VotingRightOwnerType
        Data: effective owned_ref and proposal entries
Outputs:
    - VoteMeta:
        Lock: AlwaysSuccess
        Type: VoteMetaType
        Data: updated exactly as in one base vote or revote
    - VotingRightOwner:
        Lock: full owner-cell lock script whose hash equals return_owner_cell_lock_hash
        Type: VotingRightOwnerType
        Data: owner entry added or updated
        Capacity: input VotingRightOwner capacity minus at most max_fill_fee

Optional cancel_vote_intent

CellDeps:
    - VotingRightOwnerType code cell
    - VoteIntentLock code cell
Inputs:
    - owner-auth cell under a lock whose hash matches VoteIntentLockArgs.owner_cell_lock_hash
    - VotingRightOwner intent cell:
        Lock: VoteIntentLock
        Type: VotingRightOwnerType
Witnesses:
    - owner-auth witness required by the owner-cell lock
Outputs:
    - VotingRightOwner:
        Lock: full owner-cell lock script whose hash equals return_owner_cell_lock_hash
        Type: VotingRightOwnerType
        Data: entries unchanged, owned_ref materialized if needed
        Capacity: same as input VotingRightOwner

Open question: are these transaction shapes consistent with the rule comments above, and is any proof surface missing?

2 Likes

If anybody has any question about Deposit-Paired Voting and its implementation details, I’ll be available here :hugs:

Love & Peace, Phroi

1 Like

Hi Phroi,
I can tell this is a very carefully scoped design, especially the way it separates tally correctness from governance ‘officialness’ and fair inclusion.

One small suggestion: maybe a later version could include a simple edge-case matrix for things like same-input contention on VoteMeta, late votes / revotes, pending-slot resolution, release-before-close, proposal-document witness retrieval failure, and intent-cell filler censorship.

Very promising direction.

1 Like

To vote, users must migrate all assets into a new custody cell, which may feel uncomfortable for some.