Decred 投票系统 源码分析

Decred 投票系统 源码分析

总述

Decred是一种开源,渐进,自治的加密货币,和传统区块链不同的是,decred在保留pow共识的同时,还建立了一套基于持票人的pos系统。pos投票的作用体现在三个方面。

  1. 每个区块在由矿工挖出的同时也要持票人进行投票见证,每个区块包含最少三张赞同票。可以制约矿工的不良行为,避免矿工掌握算力后为所欲为。
  2. 可以在网络上发布提案,通过持票人对该提案进行投票来决定是否进行网络升级,只有得到超过75%的赞同票时才可以升级成功。
  3. 用户参与投票提案后会得到一定量的奖励,激励用户参与社区自治。

基础流程

票的生命状态分为,未成熟(Immature),成熟(Live),已投票(Voted),过期(Expired),丢失(Missed),退票(Revoked),票生命周期如下图。

购票

用户通过质押一定的币换取票,票价通过类似pow的算法波动,全网固定权益池大小,高于容量票价上涨,低于票价票价下跌。 购票交易结构如下

其中可以看到这个交易抵押了98个dcr,换取一张票.

票的输出1的类型为stakesubmission,该output的主要作用是标记投票人,系统中通过这个地址确定这张票由谁来管理,通常情况下是自己钱包中的一个地址,也可以让别人代投。还可以通过多签地址多方共同管理投票,权益池的原理就是基于多签票地址来完成的。

输出口2标记了票的受益人,该脚本是一个nulldata脚本,其中嵌入了该地址的实际收益,只是浏览器中并未解出这个值。

输出口3用于找零,通常无用。

    func makeTicket(params *chaincfg.Params, inputPool *extendedOutPoint, input *extendedOutPoint, addrVote dcrutil.Address,
        addrSubsidy dcrutil.Address, ticketCost int64, addrPool dcrutil.Address) (*wire.MsgTx, error) {

        mtx := wire.NewMsgTx()

        if addrPool != nil && inputPool != nil {
            txIn := wire.NewTxIn(inputPool.op, inputPool.amt, []byte{})
            mtx.AddTxIn(txIn)
        }

        txIn := wire.NewTxIn(input.op, input.amt, []byte{})
        mtx.AddTxIn(txIn)

        // Create a new script which pays to the provided address with an
        // SStx tagged output.
        if addrVote == nil {
            return nil, errors.E(errors.Invalid, "nil vote address")
        }
        pkScript, err := txscript.PayToSStx(addrVote)
        if err != nil {
            return nil, errors.E(errors.Op("txscript.PayToSStx"), errors.Invalid,
                errors.Errorf("vote address %v", addrVote))
        }

        txOut := wire.NewTxOut(ticketCost, pkScript)
        txOut.Version = txscript.DefaultScriptVersion
        mtx.AddTxOut(txOut)

        // Obtain the commitment amounts.
        var amountsCommitted []int64
        userSubsidyNullIdx := 0
        if addrPool == nil {
            _, amountsCommitted, err = stake.SStxNullOutputAmounts(
                []int64{input.amt}, []int64{0}, ticketCost)
            if err != nil {
                return nil, err
            }

        } else {
            _, amountsCommitted, err = stake.SStxNullOutputAmounts(
                []int64{inputPool.amt, input.amt}, []int64{0, 0}, ticketCost)
            if err != nil {
                return nil, err
            }
            userSubsidyNullIdx = 1
        }

        // Zero value P2PKH addr.
        zeroed := [20]byte{}
        addrZeroed, err := dcrutil.NewAddressPubKeyHash(zeroed[:], params, 0)
        if err != nil {
            return nil, err
        }

        // 2. (Optional) If we're passed a pool address, make an extra
        // commitment to the pool.
        limits := uint16(defaultTicketFeeLimits)
        if addrPool != nil {
            pkScript, err = txscript.GenerateSStxAddrPush(addrPool,
                dcrutil.Amount(amountsCommitted[0]), limits)
            if err != nil {
                return nil, errors.E(errors.Op("txscript.GenerateSStxAddrPush"), errors.Invalid,
                    errors.Errorf("pool commitment address %v", addrPool))
            }
            txout := wire.NewTxOut(int64(0), pkScript)
            mtx.AddTxOut(txout)

            // Create a new script which pays to the provided address with an
            // SStx change tagged output.
            pkScript, err = txscript.PayToSStxChange(addrZeroed)
            if err != nil {
                return nil, errors.E(errors.Op("txscript.PayToSStxChange"), errors.Bug,
                    errors.Errorf("ticket change address %v", addrZeroed))
            }

            txOut = wire.NewTxOut(0, pkScript)
            txOut.Version = txscript.DefaultScriptVersion
            mtx.AddTxOut(txOut)
        }

        // 3. Create the commitment and change output paying to the user.
        //
        // Create an OP_RETURN push containing the pubkeyhash to send rewards to.
        // Apply limits to revocations for fees while not allowing
        // fees for votes.
        pkScript, err = txscript.GenerateSStxAddrPush(addrSubsidy,
            dcrutil.Amount(amountsCommitted[userSubsidyNullIdx]), limits)
        if err != nil {
            return nil, errors.E(errors.Op("txscript.GenerateSStxAddrPush"), errors.Invalid,
                errors.Errorf("commitment address %v", addrSubsidy))
        }
        txout := wire.NewTxOut(int64(0), pkScript)
        mtx.AddTxOut(txout)

        // Create a new script which pays to the provided address with an
        // SStx change tagged output.
        pkScript, err = txscript.PayToSStxChange(addrZeroed)
        if err != nil {
            return nil, errors.E(errors.Op("txscript.PayToSStxChange"), errors.Bug,
                errors.Errorf("ticket change address %v", addrZeroed))
        }

        txOut = wire.NewTxOut(0, pkScript)
        txOut.Version = txscript.DefaultScriptVersion
        mtx.AddTxOut(txOut)

        // Make sure we generated a valid SStx.
        if err := stake.CheckSStx(mtx); err != nil {
            return nil, errors.E(errors.Op("stake.CheckSStx"), errors.Bug, err)
        }

        return mtx, nil
    }

选票

每个区块产生之后,会立即在权益池里面随机选择5张票出来,称之为winningticket,之后进行全网广播,如果此时节点在线则可以进行投票,如果不在线,就会丢票。

    func (b *BlockChain) fetchNewTicketsForNode(node *blockNode) ([]chainhash.Hash, error) {
        // If we're before the stake enabled height, there can be no
        // tickets in the live ticket pool.
        if node.height < b.chainParams.StakeEnabledHeight {
            return []chainhash.Hash{}, nil
        }

        // If we already cached the tickets, simply return the cached list.
        // It's important to make the distinction here that nil means the
        // value was never looked up, while an empty slice of pointers means
        // that there were no new tickets at this height.
        if node.newTickets != nil {
            return node.newTickets, nil
        }

        // Calculate block number for where new tickets matured from and retrieve
        // this block from DB or in memory if it's a sidechain.
        matureNode := node.RelativeAncestor(int64(b.chainParams.TicketMaturity))
        if matureNode == nil {
            return nil, fmt.Errorf("unable to obtain previous node; " +
                "ancestor is genesis block")
        }

        matureBlock, errBlock := b.fetchBlockByNode(matureNode)
        if errBlock != nil {
            return nil, errBlock
        }

        tickets := []chainhash.Hash{}
        for _, stx := range matureBlock.MsgBlock().STransactions {
            if stake.IsSStx(stx) {
                h := stx.TxHash()
                tickets = append(tickets, h)
            }
        }

        // Set the new tickets in memory so that they exist for future
        // reference in the node.
        node.newTickets = tickets

        return tickets, nil
    }

投票

钱包在收到winningticket通知后,迅速构建投票交易并广播,这个时间很短,从winningticket广播,到区块开始收集交易,时间只有100ms,稍微卡一下就会丢票,这也是权益池的优势所在,可以通过多节点投票保证投票成功的几率。

输出1 标记引用区块hash,锁定了票的投票区块 输出2 包含的投票的相关信息 输出3 返回购票质押的虚拟货币以及pos收益

    func createUnsignedVote(ticketHash *chainhash.Hash, ticketPurchase *wire.MsgTx,
        blockHeight int32, blockHash *chainhash.Hash, voteBits stake.VoteBits,
        subsidyCache *blockchain.SubsidyCache, params *chaincfg.Params) (*wire.MsgTx, error) {

        // Parse the ticket purchase transaction to determine the required output
        // destinations for vote rewards or revocations.
        ticketPayKinds, ticketHash160s, ticketValues, _, _, _ :=
            stake.TxSStxStakeOutputInfo(ticketPurchase)

        // Calculate the subsidy for votes at this height.
        subsidy := blockchain.CalcStakeVoteSubsidy(subsidyCache, int64(blockHeight),
            params)

        // Calculate the output values from this vote using the subsidy.
        voteRewardValues := stake.CalculateRewards(ticketValues,
            ticketPurchase.TxOut[0].Value, subsidy)

        // Begin constructing the vote transaction.
        vote := wire.NewMsgTx()

        // Add stakebase input to the vote.
        stakebaseOutPoint := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0),
            wire.TxTreeRegular)
        stakebaseInput := wire.NewTxIn(stakebaseOutPoint, subsidy, nil)
        vote.AddTxIn(stakebaseInput)

        // Votes reference the ticket purchase with the second input.
        ticketOutPoint := wire.NewOutPoint(ticketHash, 0, wire.TxTreeStake)
        ticketInput := wire.NewTxIn(ticketOutPoint,
            ticketPurchase.TxOut[ticketOutPoint.Index].Value, nil)
        vote.AddTxIn(ticketInput)

        // The first output references the previous block the vote is voting on.
        // This function never errors.
        blockRefScript, _ := txscript.GenerateSSGenBlockRef(*blockHash,
            uint32(blockHeight))
        vote.AddTxOut(wire.NewTxOut(0, blockRefScript))

        // The second output contains the votebits encode as a null data script.
        voteScript, err := newVoteScript(voteBits)
        if err != nil {
            return nil, err
        }
        vote.AddTxOut(wire.NewTxOut(0, voteScript))

        // All remaining outputs pay to the output destinations and amounts tagged
        // by the ticket purchase.
        for i, hash160 := range ticketHash160s {
            scriptFn := txscript.PayToSSGenPKHDirect
            if ticketPayKinds[i] { // P2SH
                scriptFn = txscript.PayToSSGenSHDirect
            }
            // Error is checking for a nil hash160, just ignore it.
            script, _ := scriptFn(hash160)
            vote.AddTxOut(wire.NewTxOut(voteRewardValues[i], script))
        }

        return vote, nil
    }

过期

每张票都有一定的生命周期,该周期的长度大约时权益池大小的四倍,如果在生命周期内都没能选中,则这张票会进入退票。

    ticketExpiry := g.params.TicketExpiry
    for i := 0; i < len(g.liveTickets); i++ {
        ticket := g.liveTickets[i]
        liveHeight := ticket.blockHeight + ticketMaturity
        expireHeight := liveHeight + ticketExpiry
        if height >= expireHeight {
            g.liveTickets = removeTicket(g.liveTickets, i)
            g.expiredTickets = append(g.expiredTickets, ticket)

            // This is required because the ticket at the current
            // offset was just removed from the slice that is being
            // iterated, so adjust the offset down one accordingly.
            i--
        }
    }

退票

区块在选择winningticket的同时,也会收集上一个区块中丢失的票,并将这些票广播出去,钱包收到miss票通知时,构建退票交易返回质押的数字货币。

输出口1用于返还购票的虚拟货币

    func createUnsignedRevocation(ticketHash *chainhash.Hash, ticketPurchase *wire.MsgTx, feePerKB dcrutil.Amount) (*wire.MsgTx, error) {
        // Parse the ticket purchase transaction to determine the required output
        // destinations for vote rewards or revocations.
        ticketPayKinds, ticketHash160s, ticketValues, _, _, _ :=
            stake.TxSStxStakeOutputInfo(ticketPurchase)

        // Calculate the output values for the revocation.  Revocations do not
        // contain any subsidy.
        revocationValues := stake.CalculateRewards(ticketValues,
            ticketPurchase.TxOut[0].Value, 0)

        // Begin constructing the revocation transaction.
        revocation := wire.NewMsgTx()

        // Revocations reference the ticket purchase with the first (and only)
        // input.
        ticketOutPoint := wire.NewOutPoint(ticketHash, 0, wire.TxTreeStake)
        ticketInput := wire.NewTxIn(ticketOutPoint,
            ticketPurchase.TxOut[ticketOutPoint.Index].Value, nil)
        revocation.AddTxIn(ticketInput)
        scriptSizes := []int{txsizes.RedeemP2SHSigScriptSize}

        // All remaining outputs pay to the output destinations and amounts tagged
        // by the ticket purchase.
        for i, hash160 := range ticketHash160s {
            scriptFn := txscript.PayToSSRtxPKHDirect
            if ticketPayKinds[i] { // P2SH
                scriptFn = txscript.PayToSSRtxSHDirect
            }
            // Error is checking for a nil hash160, just ignore it.
            script, _ := scriptFn(hash160)
            revocation.AddTxOut(wire.NewTxOut(revocationValues[i], script))
        }

        // Revocations must pay a fee but do so by decreasing one of the output
        // values instead of increasing the input value and using a change output.
        // Calculate the estimated signed serialize size.
        sizeEstimate := txsizes.EstimateSerializeSize(scriptSizes, revocation.TxOut, 0)
        feeEstimate := txrules.FeeForSerializeSize(feePerKB, sizeEstimate)

        // Reduce the output value of one of the outputs to accomodate for the relay
        // fee.  To avoid creating dust outputs, a suitable output value is reduced
        // by the fee estimate only if it is large enough to not create dust.  This
        // code does not currently handle reducing the output values of multiple
        // commitment outputs to accomodate for the fee.
        for _, output := range revocation.TxOut {
            if dcrutil.Amount(output.Value) > feeEstimate {
                amount := dcrutil.Amount(output.Value) - feeEstimate
                if !txrules.IsDustAmount(amount, len(output.PkScript), feePerKB) {
                    output.Value = int64(amount)
                    return revocation, nil
                }
            }
        }
        return nil, errors.New("missing suitable revocation output to pay relay fee")
    }

提案与网络升级

总所周知,区块链升级的过程中往往伴随着分叉的风险,进而导致社区,用户的分裂,降低币的影响力。decred通过投票提案的方式规避这个问题,当需要进行网络升级的时候,社区会发布一个新的提案版本,持票人可以选择支持还是反对这个版本,随着区块高度的增长,系统会计算投票的总量,超过75%的比例后网络就会自动升级。思想同pos类似,掌握多数票的人才是实际的利益相关者,这些人才有更大的动力去维护网络的稳定安全。

发起提案

提案通常写死在配置里面,同时会有一些功能代码,以及网络升级判断开关. 以当前decred进行的闪电网络投票为例

    {{
        Vote: Vote{
            Id:          VoteIDLNFeatures,
            Description: "Enable features defined in DCP0002 and DCP0003 necessary to support Lightning Network (LN)",
            Mask:        0x0006, // Bits 1 and 2
            Choices: []Choice{{
                Id:          "abstain",     //弃权
                Description: "abstain voting for change",
                Bits:        0x0000,
                IsAbstain:   true,
                IsNo:        false,
            }, {
                Id:          "no",          //反对
                Description: "keep the existing consensus rules",
                Bits:        0x0002, // Bit 1
                IsAbstain:   false,
                IsNo:        true,
            }, {
                Id:          "yes",         //赞同
                Description: "change to the new consensus rules",
                Bits:        0x0004, // Bit 2
                IsAbstain:   false,
                IsNo:        false,
            }},
        },
        StartTime:  1505260800, // Sep 13th, 2017
        ExpireTime: 1536796800, // Sep 13th, 2018
    }},

投票选择提案

持票人可以通过接口或者界面对当前提案进行表态(支持,反对,弃权).

设置对提案的表态

    func (w *Wallet) SetAgendaChoices(choices ...AgendaChoice) (voteBits uint16, err error) {
        const op errors.Op = "wallet.SetAgendaChoices"
        version, deployments := CurrentAgendas(w.chainParams)
        if len(deployments) == 0 {
            return 0, errors.E("no agendas to set for this network")
        }

        type maskChoice struct {
            mask uint16
            bits uint16
        }
        var appliedChoices []maskChoice

        err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
            for _, c := range choices {
                var matchingAgenda *chaincfg.Vote
                for i := range deployments {
                    if deployments[i].Vote.Id == c.AgendaID {
                        matchingAgenda = &deployments[i].Vote
                        break
                    }
                }
                if matchingAgenda == nil {
                    return errors.E(errors.Invalid, errors.Errorf("no agenda with ID %q", c.AgendaID))
                }

                var matchingChoice *chaincfg.Choice
                for i := range matchingAgenda.Choices {
                    if matchingAgenda.Choices[i].Id == c.ChoiceID {
                        matchingChoice = &matchingAgenda.Choices[i]
                        break
                    }
                }
                if matchingChoice == nil {
                    return errors.E(errors.Invalid, errors.Errorf("agenda %q has no choice ID %q", c.AgendaID, c.ChoiceID))
                }

                err := udb.SetAgendaPreference(tx, version, c.AgendaID, c.ChoiceID)
                if err != nil {
                    return err
                }
                appliedChoices = append(appliedChoices, maskChoice{
                    mask: matchingAgenda.Mask,
                    bits: matchingChoice.Bits,
                })
            }
            return nil
        })
        if err != nil {
            return 0, errors.E(op, err)
        }

        // With the DB update successful, modify the actual votebits cached by the
        // wallet structure.
        w.stakeSettingsLock.Lock()
        for _, c := range appliedChoices {
            w.voteBits.Bits &^= c.mask // Clear all bits from this agenda
            w.voteBits.Bits |= c.bits  // Set bits for this choice
        }
        voteBits = w.voteBits.Bits
        w.stakeSettingsLock.Unlock()

        return voteBits, nil
    }

生成投票脚本

    func newVoteScript(voteBits stake.VoteBits) ([]byte, error) {
        b := make([]byte, 2+len(voteBits.ExtendedBits))
        binary.LittleEndian.PutUint16(b[0:2], voteBits.Bits)
        copy(b[2:], voteBits.ExtendedBits[:])
        return txscript.GenerateProvablyPruneableOut(b)
    }

    func GenerateProvablyPruneableOut(data []byte) ([]byte, error) {
        if len(data) > MaxDataCarrierSize {
            str := fmt.Sprintf("data size %d is larger than max "+
                "allowed size %d", len(data), MaxDataCarrierSize)
            return nil, scriptError(ErrTooMuchNullData, str)
        }

        return NewScriptBuilder().AddOp(OP_RETURN).AddData(data).Script()
    }

计票

矿工接受到票后,根据里面的信息判断,支持的则该票版本号升级,反对和弃权则维持老的版本号,系统会统计计票窗口内所有支持新版本的票,如果超过总票数的75%则升级成功

    func (b *BlockChain) calcVoterVersionInterval(prevNode *blockNode) (uint32, error) {
        // Ensure the provided node is the final node in a valid stake version
        // interval and is greater than or equal to the stake validation height
        // since the logic below relies on these assumptions.
        svh := b.chainParams.StakeValidationHeight
        svi := b.chainParams.StakeVersionInterval
        expectedHeight := calcWantHeight(svh, svi, prevNode.height+1)
        if prevNode.height != expectedHeight || expectedHeight < svh {
            return 0, AssertError(fmt.Sprintf("calcVoterVersionInterval "+
                "must be called with a node that is the final node "+
                "in a stake version interval -- called with node %s "+
                "(height %d)", prevNode.hash, prevNode.height))
        }

        // See if we have cached results.
        if result, ok := b.calcVoterVersionIntervalCache[prevNode.hash]; ok {
            return result, nil
        }

        // Tally both the total number of votes in the previous stake version validation
        // interval and how many of each version those votes have.
        versions := make(map[uint32]int32) // [version][count]
        totalVotesFound := int32(0)
        iterNode := prevNode
        for i := int64(0); i < svi && iterNode != nil; i++ {
            totalVotesFound += int32(len(iterNode.votes))
            for _, v := range iterNode.votes {
                versions[v.Version]++
            }

            iterNode = iterNode.parent
        }

        // Determine the required amount of votes to reach supermajority.
        numRequired := totalVotesFound * b.chainParams.StakeMajorityMultiplier /
            b.chainParams.StakeMajorityDivisor

        for version, count := range versions {
            if count >= numRequired {
                b.calcVoterVersionIntervalCache[prevNode.hash] = version
                return version, nil
            }
        }

        return 0, errVoterVersionMajorityNotFound
    }

权益池

权益池的架构由一个主节点和多个vote节点组成,主节点管理一个冷钱包,用于pos权益池收益支付.另外管理一个主公钥,通过派生该公钥生成新的地址,每个地址与一个权益池参与者构建一个多签地址,用作参与者购票的ticketaddress,这样参与人和权益池都能过够进行投票.

权益池购票交易

权益池投票交易

相对于solo购票多出一个sstxcommitment和sstxchange,一个sstxcommitment就是一个支付出口,多出来的这个保存了支付给权益池的相关信息(地址,费用).相对应的投票交易也多出一个stakegen类型输出.

引用

decred 文档

decred 浏览器

decred 项目

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区