Decred是一种开源,渐进,自治的加密货币,和传统区块链不同的是,decred在保留pow共识的同时,还建立了一套基于持票人的pos系统。pos投票的作用体现在三个方面。
票的生命状态分为,未成熟(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类型输出.