首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Decred 投票系统 源码分析

Decred 投票系统 源码分析

作者头像
魂祭心
发布2019-03-12 15:47:32
1.6K0
发布2019-03-12 15:47:32
举报
文章被收录于专栏:魂祭心魂祭心

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 项目

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018/10/10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Decred 投票系统 源码分析
    • 总述
      • 基础流程
        • 购票
        • 选票
        • 投票
        • 过期
        • 退票
      • 提案与网络升级
        • 发起提案
        • 投票选择提案
        • 计票
      • 权益池
        • 引用
        相关产品与服务
        区块链
        云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档