mirror of
https://github.com/FlipsideCrypto/dcrd.git
synced 2026-02-06 10:56:47 +00:00
2601 lines
97 KiB
Go
2601 lines
97 KiB
Go
// Copyright (c) 2016-2019 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package chaingen
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"runtime"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/chaincfg/v2"
|
|
"github.com/decred/dcrd/dcrutil/v2"
|
|
"github.com/decred/dcrd/txscript/v2"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
var (
|
|
// hash256prngSeedConst is a constant derived from the hex
|
|
// representation of pi and is used in conjunction with a caller-provided
|
|
// seed when initializing the deterministic lottery prng.
|
|
hash256prngSeedConst = []byte{0x24, 0x3f, 0x6a, 0x88, 0x85, 0xa3, 0x08,
|
|
0xd3}
|
|
|
|
// opTrueScript is a simple public key script that contains the OP_TRUE
|
|
// opcode. It is defined here to reduce garbage creation.
|
|
opTrueScript = []byte{txscript.OP_TRUE}
|
|
|
|
// opTrueRedeemScript is the signature script that can be used to redeem
|
|
// a p2sh output to the opTrueScript. It is defined here to reduce
|
|
// garbage creation.
|
|
opTrueRedeemScript = []byte{txscript.OP_DATA_1, txscript.OP_TRUE}
|
|
|
|
// coinbaseSigScript is the signature script used by the tests when
|
|
// creating standard coinbase transactions. It is defined here to
|
|
// reduce garbage creation.
|
|
coinbaseSigScript = []byte{txscript.OP_0, txscript.OP_0}
|
|
)
|
|
|
|
const (
|
|
// voteBitYes is the specific bit that is set in the vote bits to
|
|
// indicate that the previous block is valid.
|
|
voteBitYes = 0x01
|
|
)
|
|
|
|
// SpendableOut represents a transaction output that is spendable along with
|
|
// additional metadata such as the block its in and how much it pays.
|
|
type SpendableOut struct {
|
|
prevOut wire.OutPoint
|
|
blockHeight uint32
|
|
blockIndex uint32
|
|
amount dcrutil.Amount
|
|
}
|
|
|
|
// PrevOut returns the outpoint associated with the spendable output.
|
|
func (s *SpendableOut) PrevOut() wire.OutPoint {
|
|
return s.prevOut
|
|
}
|
|
|
|
// BlockHeight returns the block height of the block the spendable output is in.
|
|
func (s *SpendableOut) BlockHeight() uint32 {
|
|
return s.blockHeight
|
|
}
|
|
|
|
// BlockIndex returns the offset into the block the spendable output is in.
|
|
func (s *SpendableOut) BlockIndex() uint32 {
|
|
return s.blockIndex
|
|
}
|
|
|
|
// Amount returns the amount associated with the spendable output.
|
|
func (s *SpendableOut) Amount() dcrutil.Amount {
|
|
return s.amount
|
|
}
|
|
|
|
// makeSpendableOutForTxInternal returns a spendable output for the given
|
|
// transaction block height, transaction index within the block, transaction
|
|
// tree and transaction output index within the transaction.
|
|
func makeSpendableOutForTxInternal(tx *wire.MsgTx, blockHeight, txIndex, txOutIndex uint32, tree int8) SpendableOut {
|
|
return SpendableOut{
|
|
prevOut: wire.OutPoint{
|
|
Hash: *tx.CachedTxHash(),
|
|
Index: txOutIndex,
|
|
Tree: tree,
|
|
},
|
|
blockHeight: blockHeight,
|
|
blockIndex: txIndex,
|
|
amount: dcrutil.Amount(tx.TxOut[txOutIndex].Value),
|
|
}
|
|
}
|
|
|
|
// MakeSpendableOutForTx returns a spendable output for a regular transaction.
|
|
func MakeSpendableOutForTx(tx *wire.MsgTx, blockHeight, txIndex, txOutIndex uint32) SpendableOut {
|
|
return makeSpendableOutForTxInternal(tx, blockHeight, txIndex, txOutIndex, wire.TxTreeRegular)
|
|
}
|
|
|
|
// MakeSpendableOutForSTx returns a spendable output for a stake transaction.
|
|
func MakeSpendableOutForSTx(tx *wire.MsgTx, blockHeight, txIndex, txOutIndex uint32) SpendableOut {
|
|
return makeSpendableOutForTxInternal(tx, blockHeight, txIndex, txOutIndex, wire.TxTreeStake)
|
|
}
|
|
|
|
// MakeSpendableOut returns a spendable output for the given block, transaction
|
|
// index within the block, and transaction output index within the transaction.
|
|
func MakeSpendableOut(block *wire.MsgBlock, txIndex, txOutIndex uint32) SpendableOut {
|
|
tx := block.Transactions[txIndex]
|
|
return MakeSpendableOutForTx(tx, block.Header.Height, txIndex, txOutIndex)
|
|
}
|
|
|
|
// MakeSpendableStakeOut returns a spendable stake output for the given block,
|
|
// transaction index within the block, and transaction output index within the
|
|
// transaction.
|
|
func MakeSpendableStakeOut(block *wire.MsgBlock, txIndex, txOutIndex uint32) SpendableOut {
|
|
tx := block.STransactions[txIndex]
|
|
return MakeSpendableOutForSTx(tx, block.Header.Height, txIndex, txOutIndex)
|
|
}
|
|
|
|
// stakeTicket represents a transaction that is an sstx along with the height of
|
|
// the block it was mined in and the its index within that block.
|
|
type stakeTicket struct {
|
|
tx *wire.MsgTx
|
|
blockHeight uint32
|
|
blockIndex uint32
|
|
}
|
|
|
|
// stakeTicketSorter implements sort.Interface to allow a slice of stake tickets
|
|
// to be sorted.
|
|
type stakeTicketSorter []*stakeTicket
|
|
|
|
// Len returns the number of stake tickets in the slice. It is part of the
|
|
// sort.Interface implementation.
|
|
func (t stakeTicketSorter) Len() int { return len(t) }
|
|
|
|
// Swap swaps the stake tickets at the passed indices. It is part of the
|
|
// sort.Interface implementation.
|
|
func (t stakeTicketSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
|
|
|
// Less returns whether the stake ticket with index i should sort before the
|
|
// stake ticket with index j. It is part of the sort.Interface implementation.
|
|
func (t stakeTicketSorter) Less(i, j int) bool {
|
|
iHash := t[i].tx.CachedTxHash()[:]
|
|
jHash := t[j].tx.CachedTxHash()[:]
|
|
return bytes.Compare(iHash, jHash) < 0
|
|
}
|
|
|
|
// Generator houses state used to ease the process of generating test blocks
|
|
// that build from one another along with housing other useful things such as
|
|
// available spendable outputs and generic payment scripts used throughout the
|
|
// tests.
|
|
type Generator struct {
|
|
params *chaincfg.Params
|
|
tip *wire.MsgBlock
|
|
tipName string
|
|
blocks map[chainhash.Hash]*wire.MsgBlock
|
|
blockHeights map[chainhash.Hash]uint32
|
|
blocksByName map[string]*wire.MsgBlock
|
|
p2shOpTrueAddr dcrutil.Address
|
|
p2shOpTrueScript []byte
|
|
|
|
// Used for tracking spendable coinbase outputs.
|
|
spendableOuts [][]SpendableOut
|
|
prevCollectedHash chainhash.Hash
|
|
|
|
// Used for tracking the live ticket pool and revocations.
|
|
originalParents map[chainhash.Hash]chainhash.Hash
|
|
immatureTickets []*stakeTicket
|
|
liveTickets []*stakeTicket
|
|
wonTickets map[chainhash.Hash][]*stakeTicket
|
|
expiredTickets []*stakeTicket
|
|
revokedTickets map[chainhash.Hash][]*stakeTicket
|
|
missedVotes map[chainhash.Hash]*stakeTicket
|
|
}
|
|
|
|
// MakeGenerator returns a generator instance initialized with the genesis block
|
|
// as the tip as well as a cached generic pay-to-script-hash script for OP_TRUE.
|
|
func MakeGenerator(params *chaincfg.Params) (Generator, error) {
|
|
// Generate a generic pay-to-script-hash script that is a simple
|
|
// OP_TRUE. This allows the tests to avoid needing to generate and
|
|
// track actual public keys and signatures.
|
|
p2shOpTrueAddr, err := dcrutil.NewAddressScriptHash(opTrueScript, params)
|
|
if err != nil {
|
|
return Generator{}, err
|
|
}
|
|
p2shOpTrueScript, err := txscript.PayToAddrScript(p2shOpTrueAddr)
|
|
if err != nil {
|
|
return Generator{}, err
|
|
}
|
|
|
|
genesis := params.GenesisBlock
|
|
genesisHash := genesis.BlockHash()
|
|
return Generator{
|
|
params: params,
|
|
tip: genesis,
|
|
tipName: "genesis",
|
|
blocks: map[chainhash.Hash]*wire.MsgBlock{genesisHash: genesis},
|
|
blockHeights: map[chainhash.Hash]uint32{genesis.BlockHash(): 0},
|
|
blocksByName: map[string]*wire.MsgBlock{"genesis": genesis},
|
|
p2shOpTrueAddr: p2shOpTrueAddr,
|
|
p2shOpTrueScript: p2shOpTrueScript,
|
|
originalParents: make(map[chainhash.Hash]chainhash.Hash),
|
|
wonTickets: make(map[chainhash.Hash][]*stakeTicket),
|
|
revokedTickets: make(map[chainhash.Hash][]*stakeTicket),
|
|
missedVotes: make(map[chainhash.Hash]*stakeTicket),
|
|
}, nil
|
|
}
|
|
|
|
// Params returns the chain params associated with the generator instance.
|
|
func (g *Generator) Params() *chaincfg.Params {
|
|
return g.params
|
|
}
|
|
|
|
// Tip returns the current tip block of the generator instance.
|
|
func (g *Generator) Tip() *wire.MsgBlock {
|
|
return g.tip
|
|
}
|
|
|
|
// TipName returns the name of the current tip block of the generator instance.
|
|
func (g *Generator) TipName() string {
|
|
return g.tipName
|
|
}
|
|
|
|
// P2shOpTrueAddr returns the generator p2sh script that is composed with
|
|
// a single OP_TRUE.
|
|
func (g *Generator) P2shOpTrueAddr() dcrutil.Address {
|
|
return g.p2shOpTrueAddr
|
|
}
|
|
|
|
// BlockByName returns the block associated with the provided block name. It
|
|
// will panic if the specified block name does not exist.
|
|
func (g *Generator) BlockByName(blockName string) *wire.MsgBlock {
|
|
block, ok := g.blocksByName[blockName]
|
|
if !ok {
|
|
panic(fmt.Sprintf("block name %s does not exist", blockName))
|
|
}
|
|
return block
|
|
}
|
|
|
|
// BlockByHash returns the block associated with the provided block hash. It
|
|
// will panic if the specified block hash does not exist.
|
|
func (g *Generator) BlockByHash(hash *chainhash.Hash) *wire.MsgBlock {
|
|
block, ok := g.blocks[*hash]
|
|
if !ok {
|
|
panic(fmt.Sprintf("block with hash %s does not exist", hash))
|
|
}
|
|
return block
|
|
}
|
|
|
|
// blockHeight returns the block height associated with the provided block hash.
|
|
// It will panic if the specified block hash does not exist.
|
|
func (g *Generator) blockHeight(hash chainhash.Hash) uint32 {
|
|
height, ok := g.blockHeights[hash]
|
|
if !ok {
|
|
panic(fmt.Sprintf("no block height found for block %s", hash))
|
|
}
|
|
return height
|
|
}
|
|
|
|
// opReturnScript returns a provably-pruneable OP_RETURN script with the
|
|
// provided data.
|
|
func opReturnScript(data []byte) []byte {
|
|
builder := txscript.NewScriptBuilder()
|
|
script, err := builder.AddOp(txscript.OP_RETURN).AddData(data).Script()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return script
|
|
}
|
|
|
|
// UniqueOpReturnScript returns a standard provably-pruneable OP_RETURN script
|
|
// with a random uint64 encoded as the data.
|
|
func UniqueOpReturnScript() []byte {
|
|
rand, err := wire.RandomUint64()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
data := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(data[0:8], rand)
|
|
return opReturnScript(data)
|
|
}
|
|
|
|
// calcFullSubsidy returns the full block subsidy for the given block height.
|
|
//
|
|
// NOTE: This and the other subsidy calculation funcs intentionally are not
|
|
// using the blockchain code since the intent is to be able to generate known
|
|
// good tests which exercise that code, so it wouldn't make sense to use the
|
|
// same code to generate them.
|
|
func (g *Generator) calcFullSubsidy(blockHeight uint32) dcrutil.Amount {
|
|
iterations := int64(blockHeight) / g.params.SubsidyReductionInterval
|
|
subsidy := g.params.BaseSubsidy
|
|
for i := int64(0); i < iterations; i++ {
|
|
subsidy *= g.params.MulSubsidy
|
|
subsidy /= g.params.DivSubsidy
|
|
}
|
|
return dcrutil.Amount(subsidy)
|
|
}
|
|
|
|
// calcPoWSubsidy returns the proof-of-work subsidy portion from a given full
|
|
// subsidy, block height, and number of votes that will be included in the
|
|
// block.
|
|
//
|
|
// NOTE: This and the other subsidy calculation funcs intentionally are not
|
|
// using the blockchain code since the intent is to be able to generate known
|
|
// good tests which exercise that code, so it wouldn't make sense to use the
|
|
// same code to generate them.
|
|
func (g *Generator) calcPoWSubsidy(fullSubsidy dcrutil.Amount, blockHeight uint32, numVotes uint16) dcrutil.Amount {
|
|
powProportion := dcrutil.Amount(g.params.WorkRewardProportion)
|
|
totalProportions := dcrutil.Amount(g.params.TotalSubsidyProportions())
|
|
powSubsidy := (fullSubsidy * powProportion) / totalProportions
|
|
if int64(blockHeight) < g.params.StakeValidationHeight {
|
|
return powSubsidy
|
|
}
|
|
|
|
// Reduce the subsidy according to the number of votes.
|
|
ticketsPerBlock := dcrutil.Amount(g.params.TicketsPerBlock)
|
|
return (powSubsidy * dcrutil.Amount(numVotes)) / ticketsPerBlock
|
|
}
|
|
|
|
// calcPoSSubsidy returns the proof-of-stake subsidy portion for a given block
|
|
// height being voted on.
|
|
//
|
|
// NOTE: This and the other subsidy calculation funcs intentionally are not
|
|
// using the blockchain code since the intent is to be able to generate known
|
|
// good tests which exercise that code, so it wouldn't make sense to use the
|
|
// same code to generate them.
|
|
func (g *Generator) calcPoSSubsidy(heightVotedOn uint32) dcrutil.Amount {
|
|
if int64(heightVotedOn+1) < g.params.StakeValidationHeight {
|
|
return 0
|
|
}
|
|
|
|
fullSubsidy := g.calcFullSubsidy(heightVotedOn)
|
|
posProportion := dcrutil.Amount(g.params.StakeRewardProportion)
|
|
totalProportions := dcrutil.Amount(g.params.TotalSubsidyProportions())
|
|
return (fullSubsidy * posProportion) / totalProportions
|
|
}
|
|
|
|
// calcDevSubsidy returns the dev org subsidy portion from a given full subsidy.
|
|
//
|
|
// NOTE: This and the other subsidy calculation funcs intentionally are not
|
|
// using the blockchain code since the intent is to be able to generate known
|
|
// good tests which exercise that code, so it wouldn't make sense to use the
|
|
// same code to generate them.
|
|
func (g *Generator) calcDevSubsidy(fullSubsidy dcrutil.Amount, blockHeight uint32, numVotes uint16) dcrutil.Amount {
|
|
devProportion := dcrutil.Amount(g.params.BlockTaxProportion)
|
|
totalProportions := dcrutil.Amount(g.params.TotalSubsidyProportions())
|
|
devSubsidy := (fullSubsidy * devProportion) / totalProportions
|
|
if int64(blockHeight) < g.params.StakeValidationHeight {
|
|
return devSubsidy
|
|
}
|
|
|
|
// Reduce the subsidy according to the number of votes.
|
|
ticketsPerBlock := dcrutil.Amount(g.params.TicketsPerBlock)
|
|
return (devSubsidy * dcrutil.Amount(numVotes)) / ticketsPerBlock
|
|
}
|
|
|
|
// standardCoinbaseOpReturnScript returns a standard script suitable for use as
|
|
// the second output of a standard coinbase transaction of a new block. In
|
|
// particular, the serialized data used with the OP_RETURN starts with the block
|
|
// height and is followed by 32 bytes which are treated as 4 uint64 extra
|
|
// nonces. This implementation puts a cryptographically random value into the
|
|
// final extra nonce position. The actual format of the data after the block
|
|
// height is not defined however this effectively mirrors the actual mining code
|
|
// at the time it was written.
|
|
func standardCoinbaseOpReturnScript(blockHeight uint32) []byte {
|
|
rand, err := wire.RandomUint64()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
data := make([]byte, 36)
|
|
binary.LittleEndian.PutUint32(data[0:4], blockHeight)
|
|
binary.LittleEndian.PutUint64(data[28:36], rand)
|
|
return opReturnScript(data)
|
|
}
|
|
|
|
// addCoinbaseTxOutputs adds the following outputs to the provided transaction
|
|
// which is assumed to be a coinbase transaction:
|
|
// - First output pays the development subsidy portion to the dev org
|
|
// - Second output is a standard provably prunable data-only coinbase output
|
|
// - Third and subsequent outputs pay the pow subsidy portion to the generic
|
|
// OP_TRUE p2sh script hash
|
|
func (g *Generator) addCoinbaseTxOutputs(tx *wire.MsgTx, blockHeight uint32, devSubsidy, powSubsidy dcrutil.Amount) {
|
|
// First output is the developer subsidy.
|
|
tx.AddTxOut(&wire.TxOut{
|
|
Value: int64(devSubsidy),
|
|
Version: g.params.OrganizationPkScriptVersion,
|
|
PkScript: g.params.OrganizationPkScript,
|
|
})
|
|
|
|
// Second output is a provably prunable data-only output that is used
|
|
// to ensure the coinbase is unique.
|
|
tx.AddTxOut(wire.NewTxOut(0, standardCoinbaseOpReturnScript(blockHeight)))
|
|
|
|
// Final outputs are the proof-of-work subsidy split into more than one
|
|
// output. These are in turn used throughout the tests as inputs to
|
|
// other transactions such as ticket purchases and additional spend
|
|
// transactions.
|
|
const numPoWOutputs = 6
|
|
amount := powSubsidy / numPoWOutputs
|
|
for i := 0; i < numPoWOutputs; i++ {
|
|
if i == numPoWOutputs-1 {
|
|
amount = powSubsidy - amount*(numPoWOutputs-1)
|
|
}
|
|
tx.AddTxOut(wire.NewTxOut(int64(amount), g.p2shOpTrueScript))
|
|
}
|
|
}
|
|
|
|
// CreateCoinbaseTx returns a coinbase transaction paying an appropriate
|
|
// subsidy based on the passed block height and number of votes to the dev org
|
|
// and proof-of-work miner.
|
|
//
|
|
// See the addCoinbaseTxOutputs documentation for a breakdown of the outputs
|
|
// the transaction contains.
|
|
func (g *Generator) CreateCoinbaseTx(blockHeight uint32, numVotes uint16) *wire.MsgTx {
|
|
// Calculate the subsidy proportions based on the block height and the
|
|
// number of votes the block will include.
|
|
fullSubsidy := g.calcFullSubsidy(blockHeight)
|
|
devSubsidy := g.calcDevSubsidy(fullSubsidy, blockHeight, numVotes)
|
|
powSubsidy := g.calcPoWSubsidy(fullSubsidy, blockHeight, numVotes)
|
|
|
|
tx := wire.NewMsgTx()
|
|
tx.AddTxIn(&wire.TxIn{
|
|
// Coinbase transactions have no inputs, so previous outpoint is
|
|
// zero hash and max index.
|
|
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
|
|
wire.MaxPrevOutIndex, wire.TxTreeRegular),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(devSubsidy + powSubsidy),
|
|
BlockHeight: wire.NullBlockHeight,
|
|
BlockIndex: wire.NullBlockIndex,
|
|
SignatureScript: coinbaseSigScript,
|
|
})
|
|
|
|
g.addCoinbaseTxOutputs(tx, blockHeight, devSubsidy, powSubsidy)
|
|
|
|
return tx
|
|
}
|
|
|
|
// PurchaseCommitmentScript returns a standard provably-pruneable OP_RETURN
|
|
// commitment script suitable for use in a ticket purchase tx (sstx) using the
|
|
// provided target address, amount, and fee limits.
|
|
func PurchaseCommitmentScript(addr dcrutil.Address, amount, voteFeeLimit, revocationFeeLimit dcrutil.Amount) []byte {
|
|
// The limits are defined in terms of the closest base 2 exponent and
|
|
// a bit that must be set to specify the limit is to be applied. The
|
|
// vote fee exponent is in the bottom 8 bits, while the revocation fee
|
|
// exponent is in the upper 8 bits.
|
|
limits := uint16(0)
|
|
if voteFeeLimit != 0 {
|
|
exp := uint16(math.Ceil(math.Log2(float64(voteFeeLimit))))
|
|
limits |= (exp | 0x40)
|
|
}
|
|
if revocationFeeLimit != 0 {
|
|
exp := uint16(math.Ceil(math.Log2(float64(revocationFeeLimit))))
|
|
limits |= ((exp | 0x40) << 8)
|
|
}
|
|
|
|
// The data consists of the 20-byte raw script address for the given
|
|
// address, 8 bytes for the amount to commit to (with the upper bit flag
|
|
// set to indicate a pay-to-script-hash address), and 2 bytes for the
|
|
// fee limits.
|
|
var data [30]byte
|
|
copy(data[:], addr.ScriptAddress())
|
|
binary.LittleEndian.PutUint64(data[20:], uint64(amount))
|
|
data[27] |= 1 << 7
|
|
binary.LittleEndian.PutUint16(data[28:], limits)
|
|
script, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).
|
|
AddData(data[:]).Script()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return script
|
|
}
|
|
|
|
// CreateTicketPurchaseTx creates a new transaction that spends the provided
|
|
// output to purchase a stake submission ticket (sstx) at the given ticket
|
|
// price. Both the ticket and the change will go to a p2sh script that is
|
|
// composed with a single OP_TRUE.
|
|
//
|
|
// The transaction consists of the following outputs:
|
|
// - First output is an OP_SSTX followed by the OP_TRUE p2sh script hash
|
|
// - Second output is an OP_RETURN followed by the commitment script
|
|
// - Third output is an OP_SSTXCHANGE followed by the OP_TRUE p2sh script hash
|
|
func (g *Generator) CreateTicketPurchaseTx(spend *SpendableOut, ticketPrice, fee dcrutil.Amount) *wire.MsgTx {
|
|
// The first output is the voting rights address. This impl uses the
|
|
// standard pay-to-script-hash to an OP_TRUE.
|
|
pkScript, err := txscript.PayToSStx(g.p2shOpTrueAddr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Generate the commitment script.
|
|
commitScript := PurchaseCommitmentScript(g.p2shOpTrueAddr,
|
|
ticketPrice+fee, 0, ticketPrice)
|
|
|
|
// Calculate change and generate script to deliver it.
|
|
change := spend.amount - ticketPrice - fee
|
|
changeScript, err := txscript.PayToSStxChange(g.p2shOpTrueAddr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Generate and return the transaction spending from the provided
|
|
// spendable output with the previously described outputs.
|
|
tx := wire.NewMsgTx()
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: spend.prevOut,
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(spend.amount),
|
|
BlockHeight: spend.blockHeight,
|
|
BlockIndex: spend.blockIndex,
|
|
SignatureScript: opTrueRedeemScript,
|
|
})
|
|
tx.AddTxOut(wire.NewTxOut(int64(ticketPrice), pkScript))
|
|
tx.AddTxOut(wire.NewTxOut(0, commitScript))
|
|
tx.AddTxOut(wire.NewTxOut(int64(change), changeScript))
|
|
return tx
|
|
}
|
|
|
|
// isTicketPurchaseTx returns whether or not the passed transaction is a stake
|
|
// ticket purchase.
|
|
//
|
|
// NOTE: Like many other functions in this test code, this function
|
|
// intentionally does not use the blockchain/stake package code since the intent
|
|
// is to be able to generate known good tests which exercise that code, so it
|
|
// wouldn't make sense to use the same code to generate them. It must also be
|
|
// noted that this function is NOT robust. It is the minimum necessary needed
|
|
// by the testing framework.
|
|
func isTicketPurchaseTx(tx *wire.MsgTx) bool {
|
|
if len(tx.TxOut) == 0 {
|
|
return false
|
|
}
|
|
txOut := tx.TxOut[0]
|
|
scriptClass := txscript.GetScriptClass(txOut.Version, txOut.PkScript)
|
|
return scriptClass == txscript.StakeSubmissionTy
|
|
}
|
|
|
|
// isVoteTx returns whether or not the passed tx is a stake vote (ssgen).
|
|
//
|
|
// NOTE: Like many other functions in this test code, this function
|
|
// intentionally does not use the blockchain/stake package code since the intent
|
|
// is to be able to generate known good tests which exercise that code, so it
|
|
// wouldn't make sense to use the same code to generate them. It must also be
|
|
// noted that this function is NOT robust. It is the minimum necessary needed
|
|
// by the testing framework.
|
|
func isVoteTx(tx *wire.MsgTx) bool {
|
|
if len(tx.TxOut) < 3 {
|
|
return false
|
|
}
|
|
txOut := tx.TxOut[2]
|
|
scriptClass := txscript.GetScriptClass(txOut.Version, txOut.PkScript)
|
|
return scriptClass == txscript.StakeGenTy
|
|
}
|
|
|
|
// isRevocationTx returns whether or not the passed tx is a stake ticket
|
|
// revocation (ssrtx).
|
|
//
|
|
// NOTE: Like many other functions in this test code, this function
|
|
// intentionally does not use the blockchain/stake package code since the intent
|
|
// is to be able to generate known good tests which exercise that code, so it
|
|
// wouldn't make sense to use the same code to generate them. It must also be
|
|
// noted that this function is NOT robust. It is the minimum necessary needed
|
|
// by the testing framework.
|
|
func isRevocationTx(tx *wire.MsgTx) bool {
|
|
if len(tx.TxOut) == 0 {
|
|
return false
|
|
}
|
|
txOut := tx.TxOut[0]
|
|
scriptClass := txscript.GetScriptClass(txOut.Version, txOut.PkScript)
|
|
return scriptClass == txscript.StakeRevocationTy
|
|
}
|
|
|
|
// VoteCommitmentScript returns a standard provably-pruneable OP_RETURN script
|
|
// suitable for use in a vote tx (ssgen) given the block hash and height to vote
|
|
// on.
|
|
func VoteCommitmentScript(hash chainhash.Hash, height uint32) []byte {
|
|
// The vote commitment consists of a 32-byte hash of the block it is
|
|
// voting on along with its expected height as a 4-byte little-endian
|
|
// uint32. 32-byte hash + 4-byte uint32 = 36 bytes.
|
|
var data [36]byte
|
|
copy(data[:], hash[:])
|
|
binary.LittleEndian.PutUint32(data[32:], height)
|
|
script, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).
|
|
AddData(data[:]).Script()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return script
|
|
}
|
|
|
|
// voteBlockScript returns a standard provably-pruneable OP_RETURN script
|
|
// suitable for use in a vote tx (ssgen) given the block to vote on.
|
|
func voteBlockScript(parentBlock *wire.MsgBlock) []byte {
|
|
return VoteCommitmentScript(parentBlock.BlockHash(),
|
|
parentBlock.Header.Height)
|
|
}
|
|
|
|
// voteBitsScript returns a standard provably-pruneable OP_RETURN script
|
|
// suitable for use in a vote tx (ssgen) with the appropriate vote bits set
|
|
// depending on the provided params.
|
|
func voteBitsScript(bits uint16, voteVersion uint32) []byte {
|
|
data := make([]byte, 6)
|
|
binary.LittleEndian.PutUint16(data, bits)
|
|
binary.LittleEndian.PutUint32(data[2:], voteVersion)
|
|
if voteVersion == 0 {
|
|
data = data[:2]
|
|
}
|
|
script, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).
|
|
AddData(data).Script()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return script
|
|
}
|
|
|
|
// CreateVoteTx returns a new transaction (ssgen) paying an appropriate subsidy
|
|
// for the given block height (and the number of votes per block) as well as the
|
|
// original commitments.
|
|
//
|
|
// The transaction consists of the following outputs:
|
|
// - First output is an OP_RETURN followed by the block hash and height
|
|
// - Second output is an OP_RETURN followed by the vote bits
|
|
// - Third and subsequent outputs are the payouts according to the ticket
|
|
// commitments and the appropriate proportion of the vote subsidy.
|
|
func (g *Generator) CreateVoteTx(voteBlock *wire.MsgBlock, ticketTx *wire.MsgTx, ticketBlockHeight, ticketBlockIndex uint32) *wire.MsgTx {
|
|
// Calculate the proof-of-stake subsidy proportion based on the block
|
|
// height.
|
|
posSubsidy := g.calcPoSSubsidy(voteBlock.Header.Height)
|
|
voteSubsidy := posSubsidy / dcrutil.Amount(g.params.TicketsPerBlock)
|
|
ticketPrice := dcrutil.Amount(ticketTx.TxOut[0].Value)
|
|
|
|
// The first output is the block (hash and height) the vote is for.
|
|
blockScript := voteBlockScript(voteBlock)
|
|
|
|
// The second output is the vote bits.
|
|
voteScript := voteBitsScript(voteBitYes, 0)
|
|
|
|
// The third and subsequent outputs pay the original commitment amounts
|
|
// along with the appropriate portion of the vote subsidy. This impl
|
|
// uses the standard pay-to-script-hash to an OP_TRUE.
|
|
stakeGenScript, err := txscript.PayToSSGen(g.p2shOpTrueAddr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Generate and return the transaction with the proof-of-stake subsidy
|
|
// coinbase and spending from the provided ticket along with the
|
|
// previously described outputs.
|
|
ticketHash := ticketTx.CachedTxHash()
|
|
tx := wire.NewMsgTx()
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
|
|
wire.MaxPrevOutIndex, wire.TxTreeRegular),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(voteSubsidy),
|
|
BlockHeight: wire.NullBlockHeight,
|
|
BlockIndex: wire.NullBlockIndex,
|
|
SignatureScript: g.params.StakeBaseSigScript,
|
|
})
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(ticketHash, 0,
|
|
wire.TxTreeStake),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(ticketPrice),
|
|
BlockHeight: ticketBlockHeight,
|
|
BlockIndex: ticketBlockIndex,
|
|
SignatureScript: opTrueRedeemScript,
|
|
})
|
|
tx.AddTxOut(wire.NewTxOut(0, blockScript))
|
|
tx.AddTxOut(wire.NewTxOut(0, voteScript))
|
|
tx.AddTxOut(wire.NewTxOut(int64(voteSubsidy+ticketPrice), stakeGenScript))
|
|
return tx
|
|
}
|
|
|
|
// createVoteTxFromTicket returns a new transaction (ssgen) paying an appropriate subsidy
|
|
// for the given block height (and the number of votes per block) as well as the
|
|
// original commitments. It requires a stake ticket as a parameter.
|
|
func (g *Generator) createVoteTxFromTicket(voteBlock *wire.MsgBlock, ticket *stakeTicket) *wire.MsgTx {
|
|
return g.CreateVoteTx(voteBlock, ticket.tx, ticket.blockHeight, ticket.blockIndex)
|
|
}
|
|
|
|
// CreateRevocationTx returns a new transaction (ssrtx) refunding the ticket
|
|
// price for a ticket which either missed its vote or expired.
|
|
//
|
|
// The transaction consists of the following inputs:
|
|
// - The outpoint of the ticket that was missed or expired.
|
|
//
|
|
// The transaction consists of the following outputs:
|
|
// - The payouts according to the ticket commitments.
|
|
func (g *Generator) CreateRevocationTx(ticketTx *wire.MsgTx, ticketBlockHeight, ticketBlockIndex uint32) *wire.MsgTx {
|
|
// The outputs pay the original commitment amounts. This impl uses the
|
|
// standard pay-to-script-hash to an OP_TRUE.
|
|
revokeScript, err := txscript.PayToSSRtx(g.p2shOpTrueAddr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Generate and return the transaction spending from the provided ticket
|
|
// along with the previously described outputs.
|
|
ticketPrice := ticketTx.TxOut[0].Value
|
|
ticketHash := ticketTx.CachedTxHash()
|
|
tx := wire.NewMsgTx()
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(ticketHash, 0,
|
|
wire.TxTreeStake),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: ticketPrice,
|
|
BlockHeight: ticketBlockHeight,
|
|
BlockIndex: ticketBlockIndex,
|
|
SignatureScript: opTrueRedeemScript,
|
|
})
|
|
tx.AddTxOut(wire.NewTxOut(ticketPrice, revokeScript))
|
|
return tx
|
|
}
|
|
|
|
// CreateRevocationTxFromTicket returns a new transaction (ssrtx) refunding
|
|
// the ticket price for a ticket which either missed its vote or expired.
|
|
// It requires a stake ticket as a parameter.
|
|
func (g *Generator) createRevocationTxFromTicket(ticket *stakeTicket) *wire.MsgTx {
|
|
return g.CreateRevocationTx(ticket.tx, ticket.blockHeight, ticket.blockIndex)
|
|
}
|
|
|
|
// ancestorBlock returns the ancestor block at the provided height by following
|
|
// the chain backwards from the given block. The returned block will be nil
|
|
// when a height is requested that is after the height of the passed block.
|
|
// Also, a callback can optionally be provided that is invoked with each block
|
|
// as it traverses.
|
|
func (g *Generator) ancestorBlock(block *wire.MsgBlock, height uint32, f func(*wire.MsgBlock)) *wire.MsgBlock {
|
|
// Nothing to do if the requested height is outside of the valid
|
|
// range.
|
|
if block == nil || height > block.Header.Height {
|
|
return nil
|
|
}
|
|
|
|
// Iterate backwards until the requested height is reached.
|
|
for block != nil && block.Header.Height > height {
|
|
block = g.blocks[block.Header.PrevBlock]
|
|
if f != nil && block != nil {
|
|
f(block)
|
|
}
|
|
}
|
|
|
|
return block
|
|
}
|
|
|
|
// mergeDifficulty takes an original stake difficulty and two new, scaled
|
|
// stake difficulties, merges the new difficulties, and outputs a new
|
|
// merged stake difficulty.
|
|
func mergeDifficulty(oldDiff int64, newDiff1 int64, newDiff2 int64) int64 {
|
|
newDiff1Big := big.NewInt(newDiff1)
|
|
newDiff2Big := big.NewInt(newDiff2)
|
|
newDiff2Big.Lsh(newDiff2Big, 32)
|
|
|
|
oldDiffBig := big.NewInt(oldDiff)
|
|
oldDiffBigLSH := big.NewInt(oldDiff)
|
|
oldDiffBigLSH.Lsh(oldDiffBig, 32)
|
|
|
|
newDiff1Big.Div(oldDiffBigLSH, newDiff1Big)
|
|
newDiff2Big.Div(newDiff2Big, oldDiffBig)
|
|
|
|
// Combine the two changes in difficulty.
|
|
summedChange := big.NewInt(0)
|
|
summedChange.Set(newDiff2Big)
|
|
summedChange.Lsh(summedChange, 32)
|
|
summedChange.Div(summedChange, newDiff1Big)
|
|
summedChange.Mul(summedChange, oldDiffBig)
|
|
summedChange.Rsh(summedChange, 32)
|
|
|
|
return summedChange.Int64()
|
|
}
|
|
|
|
// limitRetarget clamps the passed new difficulty to the old one adjusted by the
|
|
// factor specified in the chain parameters. This ensures the difficulty can
|
|
// only move up or down by a limited amount.
|
|
func (g *Generator) limitRetarget(oldDiff, newDiff int64) int64 {
|
|
maxRetarget := g.params.RetargetAdjustmentFactor
|
|
switch {
|
|
case newDiff == 0:
|
|
fallthrough
|
|
case (oldDiff / newDiff) > (maxRetarget - 1):
|
|
return oldDiff / maxRetarget
|
|
case (newDiff / oldDiff) > (maxRetarget - 1):
|
|
return oldDiff * maxRetarget
|
|
}
|
|
|
|
return newDiff
|
|
}
|
|
|
|
// CalcNextRequiredDifficulty returns the required proof-of-work difficulty for
|
|
// the block after the current tip block the generator is associated with.
|
|
//
|
|
// An overview of the algorithm is as follows:
|
|
// 1) Use the proof-of-work limit for all blocks before the first retarget
|
|
// window
|
|
// 2) Use the previous block's difficulty if the next block is not at a retarget
|
|
// interval
|
|
// 3) Calculate the ideal retarget difficulty for each window based on the
|
|
// actual timespan of the window versus the target timespan and exponentially
|
|
// weight each difficulty such that the most recent window has the highest
|
|
// weight
|
|
// 4) Calculate the final retarget difficulty based on the exponential weighted
|
|
// average and ensure it is limited to the max retarget adjustment factor
|
|
func (g *Generator) CalcNextRequiredDifficulty() uint32 {
|
|
// Target difficulty before the first retarget interval is the pow
|
|
// limit.
|
|
nextHeight := g.tip.Header.Height + 1
|
|
windowSize := g.params.WorkDiffWindowSize
|
|
if int64(nextHeight) < windowSize {
|
|
return g.params.PowLimitBits
|
|
}
|
|
|
|
// Return the previous block's difficulty requirements if the next block
|
|
// is not at a difficulty retarget interval.
|
|
curDiff := int64(g.tip.Header.Bits)
|
|
if int64(nextHeight)%windowSize != 0 {
|
|
return uint32(curDiff)
|
|
}
|
|
|
|
// Calculate the ideal retarget difficulty for each window based on the
|
|
// actual time between blocks versus the target time and exponentially
|
|
// weight them.
|
|
adjustedTimespan := big.NewInt(0)
|
|
tempBig := big.NewInt(0)
|
|
weightedTimespanSum, weightSum := big.NewInt(0), big.NewInt(0)
|
|
targetTimespan := int64(g.params.TargetTimespan)
|
|
targetTimespanBig := big.NewInt(targetTimespan)
|
|
numWindows := g.params.WorkDiffWindows
|
|
weightAlpha := g.params.WorkDiffAlpha
|
|
block := g.tip
|
|
finalWindowTime := block.Header.Timestamp.UnixNano()
|
|
for i := int64(0); i < numWindows; i++ {
|
|
// Get the timestamp of the block at the start of the window and
|
|
// calculate the actual timespan accordingly. Use the target
|
|
// timespan if there are not yet enough blocks left to cover the
|
|
// window.
|
|
actualTimespan := targetTimespan
|
|
if int64(block.Header.Height) > windowSize {
|
|
for j := int64(0); j < windowSize; j++ {
|
|
block = g.blocks[block.Header.PrevBlock]
|
|
}
|
|
startWindowTime := block.Header.Timestamp.UnixNano()
|
|
actualTimespan = finalWindowTime - startWindowTime
|
|
|
|
// Set final window time for the next window.
|
|
finalWindowTime = startWindowTime
|
|
}
|
|
|
|
// Calculate the ideal retarget difficulty for the window based
|
|
// on the actual timespan and weight it exponentially by
|
|
// multiplying it by 2^(window_number) such that the most recent
|
|
// window receives the most weight.
|
|
//
|
|
// Also, since integer division is being used, shift up the
|
|
// number of new tickets 32 bits to avoid losing precision.
|
|
//
|
|
// windowWeightShift = ((numWindows - i) * weightAlpha)
|
|
// adjustedTimespan = (actualTimespan << 32) / targetTimespan
|
|
// weightedTimespanSum += adjustedTimespan << windowWeightShift
|
|
// weightSum += 1 << windowWeightShift
|
|
windowWeightShift := uint((numWindows - i) * weightAlpha)
|
|
adjustedTimespan.SetInt64(actualTimespan)
|
|
adjustedTimespan.Lsh(adjustedTimespan, 32)
|
|
adjustedTimespan.Div(adjustedTimespan, targetTimespanBig)
|
|
adjustedTimespan.Lsh(adjustedTimespan, windowWeightShift)
|
|
weightedTimespanSum.Add(weightedTimespanSum, adjustedTimespan)
|
|
weight := tempBig.SetInt64(1)
|
|
weight.Lsh(weight, windowWeightShift)
|
|
weightSum.Add(weightSum, weight)
|
|
}
|
|
|
|
// Calculate the retarget difficulty based on the exponential weighted
|
|
// average and shift the result back down 32 bits to account for the
|
|
// previous shift up in order to avoid losing precision. Then, limit it
|
|
// to the maximum allowed retarget adjustment factor.
|
|
//
|
|
// nextDiff = (weightedTimespanSum/weightSum * curDiff) >> 32
|
|
curDiffBig := tempBig.SetInt64(curDiff)
|
|
weightedTimespanSum.Div(weightedTimespanSum, weightSum)
|
|
weightedTimespanSum.Mul(weightedTimespanSum, curDiffBig)
|
|
weightedTimespanSum.Rsh(weightedTimespanSum, 32)
|
|
nextDiff := weightedTimespanSum.Int64()
|
|
nextDiff = g.limitRetarget(curDiff, nextDiff)
|
|
|
|
if nextDiff > int64(g.params.PowLimitBits) {
|
|
return g.params.PowLimitBits
|
|
}
|
|
return uint32(nextDiff)
|
|
}
|
|
|
|
// CalcNextReqStakeDifficulty returns the required stake difficulty (aka
|
|
// ticket price) for the block after the provided block the generator is
|
|
// associated with.
|
|
//
|
|
// See the documentation of CalcNextRequiredStakeDifficulty for more details.
|
|
func (g *Generator) CalcNextReqStakeDifficulty(prevBlock *wire.MsgBlock) int64 {
|
|
// Stake difficulty before any tickets could possibly be purchased is
|
|
// the minimum value.
|
|
nextHeight := prevBlock.Header.Height + 1
|
|
stakeDiffStartHeight := uint32(g.params.CoinbaseMaturity) + 1
|
|
if nextHeight < stakeDiffStartHeight {
|
|
return g.params.MinimumStakeDiff
|
|
}
|
|
|
|
// Return 0 if the current difficulty is already zero since any scaling
|
|
// of 0 is still 0. This should never really happen since there is a
|
|
// minimum stake difficulty, but the consensus code checks the condition
|
|
// just in case, so follow suit here.
|
|
curDiff := g.tip.Header.SBits
|
|
if curDiff == 0 {
|
|
return 0
|
|
}
|
|
|
|
// Return the previous block's difficulty requirements if the next block
|
|
// is not at a difficulty retarget interval.
|
|
windowSize := g.params.StakeDiffWindowSize
|
|
if int64(nextHeight)%windowSize != 0 {
|
|
return curDiff
|
|
}
|
|
|
|
// --------------------------------
|
|
// Ideal pool size retarget metric.
|
|
// --------------------------------
|
|
|
|
// Calculate the ideal retarget difficulty for each window based on the
|
|
// actual pool size in the window versus the target pool size and
|
|
// exponentially weight them.
|
|
var weightedPoolSizeSum, weightSum uint64
|
|
ticketsPerBlock := int64(g.params.TicketsPerBlock)
|
|
targetPoolSize := ticketsPerBlock * int64(g.params.TicketPoolSize)
|
|
block := prevBlock
|
|
numWindows := g.params.StakeDiffWindows
|
|
weightAlpha := g.params.StakeDiffAlpha
|
|
for i := int64(0); i < numWindows; i++ {
|
|
// Get the pool size for the block at the start of the window.
|
|
// Use zero if there are not yet enough blocks left to cover the
|
|
// window.
|
|
prevRetargetHeight := nextHeight - uint32(windowSize*(i+1))
|
|
windowPoolSize := int64(0)
|
|
block = g.ancestorBlock(block, prevRetargetHeight, nil)
|
|
if block != nil {
|
|
windowPoolSize = int64(block.Header.PoolSize)
|
|
}
|
|
|
|
// Skew the pool size by the constant weight factor specified in
|
|
// the chain parameters (which is typically the max adjustment
|
|
// factor) in order to help weight the ticket pool size versus
|
|
// tickets per block. Also, ensure the skewed pool size is a
|
|
// minimum of 1.
|
|
skewedPoolSize := targetPoolSize + (windowPoolSize-
|
|
targetPoolSize)*int64(g.params.TicketPoolSizeWeight)
|
|
if skewedPoolSize <= 0 {
|
|
skewedPoolSize = 1
|
|
}
|
|
|
|
// Calculate the ideal retarget difficulty for the window based
|
|
// on the skewed pool size and weight it exponentially by
|
|
// multiplying it by 2^(window_number) such that the most recent
|
|
// window receives the most weight.
|
|
//
|
|
// Also, since integer division is being used, shift up the
|
|
// number of new tickets 32 bits to avoid losing precision.
|
|
//
|
|
// NOTE: The real algorithm uses big ints, but these purpose
|
|
// built tests won't be using large enough values to overflow,
|
|
// so just use uint64s.
|
|
adjusted := (skewedPoolSize << 32) / targetPoolSize
|
|
adjusted <<= uint64((numWindows - i) * weightAlpha)
|
|
weightedPoolSizeSum += uint64(adjusted)
|
|
weightSum += 1 << uint64((numWindows-i)*weightAlpha)
|
|
}
|
|
|
|
// Calculate the pool size retarget difficulty based on the exponential
|
|
// weighted average and shift the result back down 32 bits to account
|
|
// for the previous shift up in order to avoid losing precision. Then,
|
|
// limit it to the maximum allowed retarget adjustment factor.
|
|
//
|
|
// This is the first metric used in the final calculated difficulty.
|
|
nextPoolSizeDiff := (int64(weightedPoolSizeSum/weightSum) * curDiff) >> 32
|
|
nextPoolSizeDiff = g.limitRetarget(curDiff, nextPoolSizeDiff)
|
|
|
|
// -----------------------------------------
|
|
// Ideal tickets per window retarget metric.
|
|
// -----------------------------------------
|
|
|
|
// Calculate the ideal retarget difficulty for each window based on the
|
|
// actual number of new tickets in the window versus the target tickets
|
|
// per window and exponentially weight them.
|
|
var weightedTicketsSum uint64
|
|
targetTicketsPerWindow := ticketsPerBlock * windowSize
|
|
block = prevBlock
|
|
for i := int64(0); i < numWindows; i++ {
|
|
// Since the difficulty for the next block after the current tip
|
|
// is being calculated and there is no such block yet, the sum
|
|
// of all new tickets in the first window needs to start with
|
|
// the number of new tickets in the tip block.
|
|
var windowNewTickets int64
|
|
if i == 0 {
|
|
windowNewTickets = int64(block.Header.FreshStake)
|
|
}
|
|
|
|
// Tally all of the new tickets in all blocks in the window and
|
|
// ensure the number of new tickets is a minimum of 1.
|
|
prevRetargetHeight := nextHeight - uint32(windowSize*(i+1))
|
|
block = g.ancestorBlock(block, prevRetargetHeight, func(blk *wire.MsgBlock) {
|
|
windowNewTickets += int64(blk.Header.FreshStake)
|
|
})
|
|
if windowNewTickets <= 0 {
|
|
windowNewTickets = 1
|
|
}
|
|
|
|
// Calculate the ideal retarget difficulty for the window based
|
|
// on the number of new tickets and weight it exponentially by
|
|
// multiplying it by 2^(window_number) such that the most recent
|
|
// window receives the most weight.
|
|
//
|
|
// Also, since integer division is being used, shift up the
|
|
// number of new tickets 32 bits to avoid losing precision.
|
|
//
|
|
// NOTE: The real algorithm uses big ints, but these purpose
|
|
// built tests won't be using large enough values to overflow,
|
|
// so just use uint64s.
|
|
adjusted := (windowNewTickets << 32) / targetTicketsPerWindow
|
|
adjusted <<= uint64((numWindows - i) * weightAlpha)
|
|
weightedTicketsSum += uint64(adjusted)
|
|
}
|
|
|
|
// Calculate the tickets per window retarget difficulty based on the
|
|
// exponential weighted average and shift the result back down 32 bits
|
|
// to account for the previous shift up in order to avoid losing
|
|
// precision. Then, limit it to the maximum allowed retarget adjustment
|
|
// factor.
|
|
//
|
|
// This is the second metric used in the final calculated difficulty.
|
|
nextNewTixDiff := (int64(weightedTicketsSum/weightSum) * curDiff) >> 32
|
|
nextNewTixDiff = g.limitRetarget(curDiff, nextNewTixDiff)
|
|
|
|
// Average the previous two metrics using scaled multiplication and
|
|
// ensure the result is limited to both the maximum allowed retarget
|
|
// adjustment factor and the minimum allowed stake difficulty.
|
|
nextDiff := mergeDifficulty(curDiff, nextPoolSizeDiff, nextNewTixDiff)
|
|
nextDiff = g.limitRetarget(curDiff, nextDiff)
|
|
if nextDiff < g.params.MinimumStakeDiff {
|
|
return g.params.MinimumStakeDiff
|
|
}
|
|
return nextDiff
|
|
}
|
|
|
|
// CalcNextRequiredStakeDifficulty returns the required stake difficulty (aka
|
|
// ticket price) for the block after the current tip block the generator is
|
|
// associated with.
|
|
//
|
|
// An overview of the algorithm is as follows:
|
|
// 1) Use the minimum value for any blocks before any tickets could have
|
|
// possibly been purchased due to coinbase maturity requirements
|
|
// 2) Return 0 if the current tip block stake difficulty is 0. This is a
|
|
// safety check against a condition that should never actually happen.
|
|
// 3) Use the previous block's difficulty if the next block is not at a retarget
|
|
// interval
|
|
// 4) Calculate the ideal retarget difficulty for each window based on the
|
|
// actual pool size in the window versus the target pool size skewed by a
|
|
// constant factor to weight the ticket pool size instead of the tickets per
|
|
// block and exponentially weight each difficulty such that the most recent
|
|
// window has the highest weight
|
|
// 5) Calculate the pool size retarget difficulty based on the exponential
|
|
// weighted average and ensure it is limited to the max retarget adjustment
|
|
// factor -- This is the first metric used to calculate the final difficulty
|
|
// 6) Calculate the ideal retarget difficulty for each window based on the
|
|
// actual new tickets in the window versus the target new tickets per window
|
|
// and exponentially weight each difficulty such that the most recent window
|
|
// has the highest weight
|
|
// 7) Calculate the tickets per window retarget difficulty based on the
|
|
// exponential weighted average and ensure it is limited to the max retarget
|
|
// adjustment factor
|
|
// 8) Calculate the final difficulty by averaging the pool size retarget
|
|
// difficulty from #5 and the tickets per window retarget difficulty from #7
|
|
// using scaled multiplication and ensure it is limited to the max retarget
|
|
// adjustment factor
|
|
//
|
|
// NOTE: In order to simplify the test code, this implementation does not use
|
|
// big integers so it will NOT match the actual consensus code for really big
|
|
// numbers. However, the parameters on simnet and the pool sizes used in these
|
|
// tests are low enough that this is not an issue for the tests. Anyone looking
|
|
// at this code should NOT use it for mainnet calculations as is since it will
|
|
// not always yield the correct results.
|
|
func (g *Generator) CalcNextRequiredStakeDifficulty() int64 {
|
|
return g.CalcNextReqStakeDifficulty(g.tip)
|
|
}
|
|
|
|
// hash256prng is a deterministic pseudorandom number generator that uses a
|
|
// 256-bit secure hashing function to generate random uint32s starting from
|
|
// an initial seed.
|
|
type hash256prng struct {
|
|
seed chainhash.Hash // Initialization seed
|
|
idx uint64 // Hash iterator index
|
|
cachedHash chainhash.Hash // Most recently generated hash
|
|
hashOffset int // Offset into most recently generated hash
|
|
}
|
|
|
|
// newHash256PRNG creates a pointer to a newly created hash256PRNG.
|
|
func newHash256PRNG(seed []byte) *hash256prng {
|
|
// The provided seed is initialized by appending a constant derived from
|
|
// the hex representation of pi and hashing the result to give 32 bytes.
|
|
// This ensures the PRNG is always doing a short number of rounds
|
|
// regardless of input since it will only need to hash small messages
|
|
// (less than 64 bytes).
|
|
seedHash := chainhash.HashFunc(append(seed, hash256prngSeedConst...))
|
|
return &hash256prng{
|
|
seed: seedHash,
|
|
idx: 0,
|
|
cachedHash: seedHash,
|
|
}
|
|
}
|
|
|
|
// State returns a hash that represents the current state of the deterministic
|
|
// PRNG.
|
|
func (hp *hash256prng) State() chainhash.Hash {
|
|
// The final state is the hash of the most recently generated hash
|
|
// concatenated with both the hash iterator index and the offset into
|
|
// the hash.
|
|
//
|
|
// hash(hp.cachedHash || hp.idx || hp.hashOffset)
|
|
finalState := make([]byte, len(hp.cachedHash)+4+1)
|
|
copy(finalState, hp.cachedHash[:])
|
|
offset := len(hp.cachedHash)
|
|
binary.BigEndian.PutUint32(finalState[offset:], uint32(hp.idx))
|
|
offset += 4
|
|
finalState[offset] = byte(hp.hashOffset)
|
|
return chainhash.HashH(finalState)
|
|
}
|
|
|
|
// Hash256Rand returns a uint32 random number using the pseudorandom number
|
|
// generator and updates the state.
|
|
func (hp *hash256prng) Hash256Rand() uint32 {
|
|
offset := hp.hashOffset * 4
|
|
r := binary.BigEndian.Uint32(hp.cachedHash[offset : offset+4])
|
|
hp.hashOffset++
|
|
|
|
// Generate a new hash and reset the hash position index once it would
|
|
// overflow the available bytes in the most recently generated hash.
|
|
if hp.hashOffset > 7 {
|
|
// Hash of the seed concatenated with the hash iterator index.
|
|
// hash(hp.seed || hp.idx)
|
|
data := make([]byte, len(hp.seed)+4)
|
|
copy(data, hp.seed[:])
|
|
binary.BigEndian.PutUint32(data[len(hp.seed):], uint32(hp.idx))
|
|
hp.cachedHash = chainhash.HashH(data)
|
|
hp.idx++
|
|
hp.hashOffset = 0
|
|
}
|
|
|
|
// Roll over the entire PRNG by re-hashing the seed when the hash
|
|
// iterator index overflows a uint32.
|
|
if hp.idx > math.MaxUint32 {
|
|
hp.seed = chainhash.HashH(hp.seed[:])
|
|
hp.cachedHash = hp.seed
|
|
hp.idx = 0
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// uniformRandom returns a random in the range [0, upperBound) while avoiding
|
|
// modulo bias to ensure a normal distribution within the specified range.
|
|
func (hp *hash256prng) uniformRandom(upperBound uint32) uint32 {
|
|
if upperBound < 2 {
|
|
return 0
|
|
}
|
|
|
|
// (2^32 - (x*2)) % x == 2^32 % x when x <= 2^31
|
|
min := ((math.MaxUint32 - (upperBound * 2)) + 1) % upperBound
|
|
if upperBound > 0x80000000 {
|
|
min = 1 + ^upperBound
|
|
}
|
|
|
|
r := hp.Hash256Rand()
|
|
for r < min {
|
|
r = hp.Hash256Rand()
|
|
}
|
|
return r % upperBound
|
|
}
|
|
|
|
// winningTickets returns a slice of tickets that are required to vote for the
|
|
// given block being voted on and live ticket pool and the associated underlying
|
|
// deterministic prng state hash.
|
|
func winningTickets(voteBlock *wire.MsgBlock, liveTickets []*stakeTicket, numVotes uint16) ([]*stakeTicket, chainhash.Hash, error) {
|
|
// Serialize the parent block header used as the seed to the
|
|
// deterministic pseudo random number generator for vote selection.
|
|
var buf bytes.Buffer
|
|
buf.Grow(wire.MaxBlockHeaderPayload)
|
|
if err := voteBlock.Header.Serialize(&buf); err != nil {
|
|
return nil, chainhash.Hash{}, err
|
|
}
|
|
|
|
// Ensure the number of live tickets is within the allowable range.
|
|
numLiveTickets := uint32(len(liveTickets))
|
|
if numLiveTickets > math.MaxUint32 {
|
|
return nil, chainhash.Hash{}, fmt.Errorf("live ticket pool "+
|
|
"has %d tickets which is more than the max allowed of "+
|
|
"%d", len(liveTickets), uint32(math.MaxUint32))
|
|
}
|
|
if uint32(numVotes) > numLiveTickets {
|
|
return nil, chainhash.Hash{}, fmt.Errorf("live ticket pool "+
|
|
"has %d tickets, while %d are needed to vote",
|
|
len(liveTickets), numVotes)
|
|
}
|
|
|
|
// Construct list of winners by generating successive values from the
|
|
// deterministic prng and using them as indices into the sorted live
|
|
// ticket pool while skipping any duplicates that might occur.
|
|
prng := newHash256PRNG(buf.Bytes())
|
|
winners := make([]*stakeTicket, 0, numVotes)
|
|
usedOffsets := make(map[uint32]struct{})
|
|
for uint16(len(winners)) < numVotes {
|
|
ticketIndex := prng.uniformRandom(numLiveTickets)
|
|
if _, exists := usedOffsets[ticketIndex]; !exists {
|
|
usedOffsets[ticketIndex] = struct{}{}
|
|
winners = append(winners, liveTickets[ticketIndex])
|
|
}
|
|
}
|
|
return winners, prng.State(), nil
|
|
}
|
|
|
|
// calcFinalLotteryState calculates the final lottery state for a set of winning
|
|
// tickets and the associated deterministic prng state hash after selecting the
|
|
// winners. It is the first 6 bytes of:
|
|
// blake256(firstTicketHash || ... || lastTicketHash || prngStateHash)
|
|
func calcFinalLotteryState(winners []*stakeTicket, prngStateHash chainhash.Hash) [6]byte {
|
|
data := make([]byte, (len(winners)+1)*chainhash.HashSize)
|
|
for i := 0; i < len(winners); i++ {
|
|
h := winners[i].tx.CachedTxHash()
|
|
copy(data[chainhash.HashSize*i:], h[:])
|
|
}
|
|
copy(data[chainhash.HashSize*len(winners):], prngStateHash[:])
|
|
dataHash := chainhash.HashH(data)
|
|
|
|
var finalState [6]byte
|
|
copy(finalState[:], dataHash[0:6])
|
|
return finalState
|
|
}
|
|
|
|
// nextPowerOfTwo returns the next highest power of two from a given number if
|
|
// it is not already a power of two. This is a helper function used during the
|
|
// calculation of a merkle tree.
|
|
func nextPowerOfTwo(n int) int {
|
|
// Return the number if it's already a power of 2.
|
|
if n&(n-1) == 0 {
|
|
return n
|
|
}
|
|
|
|
// Figure out and return the next power of two.
|
|
exponent := uint(math.Log2(float64(n))) + 1
|
|
return 1 << exponent // 2^exponent
|
|
}
|
|
|
|
// hashMerkleBranches takes two hashes, treated as the left and right tree
|
|
// nodes, and returns the hash of their concatenation. This is a helper
|
|
// function used to aid in the generation of a merkle tree.
|
|
func hashMerkleBranches(left *chainhash.Hash, right *chainhash.Hash) *chainhash.Hash {
|
|
// Concatenate the left and right nodes.
|
|
var hash [chainhash.HashSize * 2]byte
|
|
copy(hash[:chainhash.HashSize], left[:])
|
|
copy(hash[chainhash.HashSize:], right[:])
|
|
|
|
newHash := chainhash.HashH(hash[:])
|
|
return &newHash
|
|
}
|
|
|
|
// buildMerkleTreeStore creates a merkle tree from a slice of transactions,
|
|
// stores it using a linear array, and returns a slice of the backing array. A
|
|
// linear array was chosen as opposed to an actual tree structure since it uses
|
|
// about half as much memory. The following describes a merkle tree and how it
|
|
// is stored in a linear array.
|
|
//
|
|
// A merkle tree is a tree in which every non-leaf node is the hash of its
|
|
// children nodes. A diagram depicting how this works for Decred transactions
|
|
// where h(x) is a blake256 hash follows:
|
|
//
|
|
// root = h1234 = h(h12 + h34)
|
|
// / \
|
|
// h12 = h(h1 + h2) h34 = h(h3 + h4)
|
|
// / \ / \
|
|
// h1 = h(tx1) h2 = h(tx2) h3 = h(tx3) h4 = h(tx4)
|
|
//
|
|
// The above stored as a linear array is as follows:
|
|
//
|
|
// [h1 h2 h3 h4 h12 h34 root]
|
|
//
|
|
// As the above shows, the merkle root is always the last element in the array.
|
|
//
|
|
// The number of inputs is not always a power of two which results in a
|
|
// balanced tree structure as above. In that case, parent nodes with no
|
|
// children are also zero and parent nodes with only a single left node
|
|
// are calculated by concatenating the left node with itself before hashing.
|
|
// Since this function uses nodes that are pointers to the hashes, empty nodes
|
|
// will be nil.
|
|
func buildMerkleTreeStore(transactions []*dcrutil.Tx) []*chainhash.Hash {
|
|
// If there's an empty stake tree, return totally zeroed out merkle tree root
|
|
// only.
|
|
if len(transactions) == 0 {
|
|
merkles := make([]*chainhash.Hash, 1)
|
|
merkles[0] = &chainhash.Hash{}
|
|
return merkles
|
|
}
|
|
|
|
// Calculate how many entries are required to hold the binary merkle
|
|
// tree as a linear array and create an array of that size.
|
|
nextPoT := nextPowerOfTwo(len(transactions))
|
|
arraySize := nextPoT*2 - 1
|
|
merkles := make([]*chainhash.Hash, arraySize)
|
|
|
|
// Create the base transaction hashes and populate the array with them.
|
|
for i, tx := range transactions {
|
|
msgTx := tx.MsgTx()
|
|
txHashFull := msgTx.TxHashFull()
|
|
merkles[i] = &txHashFull
|
|
}
|
|
|
|
// Start the array offset after the last transaction and adjusted to the
|
|
// next power of two.
|
|
offset := nextPoT
|
|
for i := 0; i < arraySize-1; i += 2 {
|
|
switch {
|
|
// When there is no left child node, the parent is nil too.
|
|
case merkles[i] == nil:
|
|
merkles[offset] = nil
|
|
|
|
// When there is no right child, the parent is generated by
|
|
// hashing the concatenation of the left child with itself.
|
|
case merkles[i+1] == nil:
|
|
newHash := hashMerkleBranches(merkles[i], merkles[i])
|
|
merkles[offset] = newHash
|
|
|
|
// The normal case sets the parent node to the hash of the
|
|
// concatenation of the left and right children.
|
|
default:
|
|
newHash := hashMerkleBranches(merkles[i], merkles[i+1])
|
|
merkles[offset] = newHash
|
|
}
|
|
offset++
|
|
}
|
|
|
|
return merkles
|
|
}
|
|
|
|
// calcMerkleRoot creates a merkle tree from the slice of transactions and
|
|
// returns the root of the tree.
|
|
func calcMerkleRoot(txns []*wire.MsgTx) chainhash.Hash {
|
|
utilTxns := make([]*dcrutil.Tx, 0, len(txns))
|
|
for _, tx := range txns {
|
|
utilTxns = append(utilTxns, dcrutil.NewTx(tx))
|
|
}
|
|
merkles := buildMerkleTreeStore(utilTxns)
|
|
return *merkles[len(merkles)-1]
|
|
}
|
|
|
|
// hashToBig converts a chainhash.Hash into a big.Int that can be used to
|
|
// perform math comparisons.
|
|
func hashToBig(hash *chainhash.Hash) *big.Int {
|
|
// A Hash is in little-endian, but the big package wants the bytes in
|
|
// big-endian, so reverse them.
|
|
buf := *hash
|
|
blen := len(buf)
|
|
for i := 0; i < blen/2; i++ {
|
|
buf[i], buf[blen-1-i] = buf[blen-1-i], buf[i]
|
|
}
|
|
|
|
return new(big.Int).SetBytes(buf[:])
|
|
}
|
|
|
|
// compactToBig converts a compact representation of a whole number N to an
|
|
// unsigned 32-bit number. The representation is similar to IEEE754 floating
|
|
// point numbers.
|
|
//
|
|
// Like IEEE754 floating point, there are three basic components: the sign,
|
|
// the exponent, and the mantissa. They are broken out as follows:
|
|
//
|
|
// * the most significant 8 bits represent the unsigned base 256 exponent
|
|
// * bit 23 (the 24th bit) represents the sign bit
|
|
// * the least significant 23 bits represent the mantissa
|
|
//
|
|
// -------------------------------------------------
|
|
// | Exponent | Sign | Mantissa |
|
|
// -------------------------------------------------
|
|
// | 8 bits [31-24] | 1 bit [23] | 23 bits [22-00] |
|
|
// -------------------------------------------------
|
|
//
|
|
// The formula to calculate N is:
|
|
// N = (-1^sign) * mantissa * 256^(exponent-3)
|
|
//
|
|
// This compact form is only used in Decred to encode unsigned 256-bit numbers
|
|
// which represent difficulty targets, thus there really is not a need for a
|
|
// sign bit, but it is implemented here to stay consistent with bitcoind.
|
|
func compactToBig(compact uint32) *big.Int {
|
|
// Extract the mantissa, sign bit, and exponent.
|
|
mantissa := compact & 0x007fffff
|
|
isNegative := compact&0x00800000 != 0
|
|
exponent := uint(compact >> 24)
|
|
|
|
// Since the base for the exponent is 256, the exponent can be treated
|
|
// as the number of bytes to represent the full 256-bit number. So,
|
|
// treat the exponent as the number of bytes and shift the mantissa
|
|
// right or left accordingly. This is equivalent to:
|
|
// N = mantissa * 256^(exponent-3)
|
|
var bn *big.Int
|
|
if exponent <= 3 {
|
|
mantissa >>= 8 * (3 - exponent)
|
|
bn = big.NewInt(int64(mantissa))
|
|
} else {
|
|
bn = big.NewInt(int64(mantissa))
|
|
bn.Lsh(bn, 8*(exponent-3))
|
|
}
|
|
|
|
// Make it negative if the sign bit is set.
|
|
if isNegative {
|
|
bn = bn.Neg(bn)
|
|
}
|
|
|
|
return bn
|
|
}
|
|
|
|
// IsSolved returns whether or not the header hashes to a value that is less
|
|
// than or equal to the target difficulty as specified by its bits field.
|
|
func IsSolved(header *wire.BlockHeader) bool {
|
|
targetDifficulty := compactToBig(header.Bits)
|
|
hash := header.BlockHash()
|
|
return hashToBig(&hash).Cmp(targetDifficulty) <= 0
|
|
}
|
|
|
|
// solveBlock attempts to find a nonce which makes the passed block header hash
|
|
// to a value less than the target difficulty. When a successful solution is
|
|
// found, true is returned and the nonce field of the passed header is updated
|
|
// with the solution. False is returned if no solution exists.
|
|
//
|
|
// NOTE: This function will never solve blocks with a nonce of 0. This is done
|
|
// so the 'NextBlock' function can properly detect when a nonce was modified by
|
|
// a munge function.
|
|
func solveBlock(header *wire.BlockHeader) bool {
|
|
// sbResult is used by the solver goroutines to send results.
|
|
type sbResult struct {
|
|
found bool
|
|
nonce uint32
|
|
}
|
|
|
|
// solver accepts a block header and a nonce range to test. It is
|
|
// intended to be run as a goroutine.
|
|
targetDifficulty := compactToBig(header.Bits)
|
|
quit := make(chan bool)
|
|
results := make(chan sbResult)
|
|
solver := func(hdr wire.BlockHeader, startNonce, stopNonce uint32) {
|
|
// We need to modify the nonce field of the header, so make sure
|
|
// we work with a copy of the original header.
|
|
for i := startNonce; i >= startNonce && i <= stopNonce; i++ {
|
|
select {
|
|
case <-quit:
|
|
results <- sbResult{false, 0}
|
|
return
|
|
default:
|
|
hdr.Nonce = i
|
|
hash := hdr.BlockHash()
|
|
if hashToBig(&hash).Cmp(
|
|
targetDifficulty) <= 0 {
|
|
|
|
results <- sbResult{true, i}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
results <- sbResult{false, 0}
|
|
}
|
|
|
|
startNonce := uint32(1)
|
|
stopNonce := uint32(math.MaxUint32)
|
|
numCores := uint32(runtime.NumCPU())
|
|
noncesPerCore := (stopNonce - startNonce) / numCores
|
|
for i := uint32(0); i < numCores; i++ {
|
|
rangeStart := startNonce + (noncesPerCore * i)
|
|
rangeStop := startNonce + (noncesPerCore * (i + 1)) - 1
|
|
if i == numCores-1 {
|
|
rangeStop = stopNonce
|
|
}
|
|
go solver(*header, rangeStart, rangeStop)
|
|
}
|
|
var foundResult bool
|
|
for i := uint32(0); i < numCores; i++ {
|
|
result := <-results
|
|
if !foundResult && result.found {
|
|
close(quit)
|
|
header.Nonce = result.nonce
|
|
foundResult = true
|
|
}
|
|
}
|
|
|
|
return foundResult
|
|
}
|
|
|
|
// ReplaceWithNVotes returns a function that itself takes a block and modifies
|
|
// it by replacing the votes in the stake tree with specified number of votes.
|
|
//
|
|
// NOTE: This must only be used as a munger to the 'NextBlock' function or it
|
|
// will lead to an invalid live ticket pool. To help safeguard against improper
|
|
// usage, it will panic if called with a block that does not connect to the
|
|
// current tip block.
|
|
func (g *Generator) ReplaceWithNVotes(numVotes uint16) func(*wire.MsgBlock) {
|
|
return func(b *wire.MsgBlock) {
|
|
// Attempt to prevent misuse of this function by ensuring the
|
|
// provided block connects to the current tip.
|
|
if b.Header.PrevBlock != g.tip.BlockHash() {
|
|
panic(fmt.Sprintf("attempt to replace number of votes "+
|
|
"for block %s with parent %s that is not the "+
|
|
"current tip %s", b.BlockHash(),
|
|
b.Header.PrevBlock, g.tip.BlockHash()))
|
|
}
|
|
|
|
// Get the winning tickets for the specified number of votes.
|
|
parentBlock := g.tip
|
|
winners, _, err := winningTickets(parentBlock, g.liveTickets,
|
|
numVotes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Generate vote transactions for the winning tickets.
|
|
defaultNumVotes := int(g.params.TicketsPerBlock)
|
|
numExisting := len(b.STransactions) - defaultNumVotes
|
|
stakeTxns := make([]*wire.MsgTx, 0, numExisting+int(numVotes))
|
|
for _, ticket := range winners {
|
|
voteTx := g.createVoteTxFromTicket(parentBlock, ticket)
|
|
stakeTxns = append(stakeTxns, voteTx)
|
|
}
|
|
|
|
// Add back the original stake transactions other than the
|
|
// original stake votes that have been replaced.
|
|
stakeTxns = append(stakeTxns, b.STransactions[defaultNumVotes:]...)
|
|
|
|
// Update the block with the new stake transactions and the
|
|
// header with the new number of votes.
|
|
b.STransactions = stakeTxns
|
|
b.Header.Voters = numVotes
|
|
|
|
// Recalculate the coinbase amount based on the number of new
|
|
// votes and update the coinbase so that the adjustment in
|
|
// subsidy is accounted for.
|
|
height := b.Header.Height
|
|
fullSubsidy := g.calcFullSubsidy(height)
|
|
devSubsidy := g.calcDevSubsidy(fullSubsidy, height, numVotes)
|
|
powSubsidy := g.calcPoWSubsidy(fullSubsidy, height, numVotes)
|
|
cbTx := b.Transactions[0]
|
|
cbTx.TxIn[0].ValueIn = int64(devSubsidy + powSubsidy)
|
|
cbTx.TxOut = nil
|
|
g.addCoinbaseTxOutputs(cbTx, height, devSubsidy, powSubsidy)
|
|
}
|
|
}
|
|
|
|
// ReplaceVoteBitsN returns a function that itself takes a block and modifies
|
|
// it by replacing the vote bits of the vote located at the provided index. It
|
|
// will panic if the stake transaction at the provided index is not already a
|
|
// vote.
|
|
//
|
|
// NOTE: This must only be used as a munger to the 'NextBlock' function or it
|
|
// will lead to an invalid live ticket pool.
|
|
func (g *Generator) ReplaceVoteBitsN(voteNum int, voteBits uint16) func(*wire.MsgBlock) {
|
|
return func(b *wire.MsgBlock) {
|
|
// Attempt to prevent misuse of this function by ensuring the
|
|
// provided stake transaction number is actually a vote.
|
|
stx := b.STransactions[voteNum]
|
|
if !isVoteTx(stx) {
|
|
panic(fmt.Sprintf("attempt to replace non-vote "+
|
|
"transaction #%d for block %s", voteNum,
|
|
b.BlockHash()))
|
|
}
|
|
|
|
// Extract the existing vote version.
|
|
existingScript := stx.TxOut[1].PkScript
|
|
var voteVersion uint32
|
|
if len(existingScript) >= 8 {
|
|
voteVersion = binary.LittleEndian.Uint32(existingScript[4:8])
|
|
}
|
|
|
|
stx.TxOut[1].PkScript = voteBitsScript(voteBits, voteVersion)
|
|
}
|
|
}
|
|
|
|
// ReplaceBlockVersion returns a function that itself takes a block and modifies
|
|
// it by replacing the stake version of the header.
|
|
func ReplaceBlockVersion(newVersion int32) func(*wire.MsgBlock) {
|
|
return func(b *wire.MsgBlock) {
|
|
b.Header.Version = newVersion
|
|
}
|
|
}
|
|
|
|
// ReplaceStakeVersion returns a function that itself takes a block and modifies
|
|
// it by replacing the stake version of the header.
|
|
func ReplaceStakeVersion(newVersion uint32) func(*wire.MsgBlock) {
|
|
return func(b *wire.MsgBlock) {
|
|
b.Header.StakeVersion = newVersion
|
|
}
|
|
}
|
|
|
|
// ReplaceVoteVersions returns a function that itself takes a block and modifies
|
|
// it by replacing the voter version of the stake transactions.
|
|
//
|
|
// NOTE: This must only be used as a munger to the 'NextBlock' function or it
|
|
// will lead to an invalid live ticket pool.
|
|
func ReplaceVoteVersions(newVersion uint32) func(*wire.MsgBlock) {
|
|
return func(b *wire.MsgBlock) {
|
|
for _, stx := range b.STransactions {
|
|
if isVoteTx(stx) {
|
|
stx.TxOut[1].PkScript = voteBitsScript(
|
|
voteBitYes, newVersion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReplaceVotes returns a function that itself takes a block and modifies it by
|
|
// replacing the voter version and bits of the stake transactions.
|
|
//
|
|
// NOTE: This must only be used as a munger to the 'NextBlock' function or it
|
|
// will lead to an invalid live ticket pool.
|
|
func ReplaceVotes(voteBits uint16, newVersion uint32) func(*wire.MsgBlock) {
|
|
return func(b *wire.MsgBlock) {
|
|
for _, stx := range b.STransactions {
|
|
if isVoteTx(stx) {
|
|
stx.TxOut[1].PkScript = voteBitsScript(voteBits,
|
|
newVersion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// CreateSpendTx creates a transaction that spends from the provided spendable
|
|
// output and includes an additional unique OP_RETURN output to ensure the
|
|
// transaction ends up with a unique hash. The public key script is a simple
|
|
// OP_TRUE p2sh script which avoids the need to track addresses and signature
|
|
// scripts in the tests. The signature script is the opTrueRedeemScript.
|
|
func (g *Generator) CreateSpendTx(spend *SpendableOut, fee dcrutil.Amount) *wire.MsgTx {
|
|
spendTx := wire.NewMsgTx()
|
|
spendTx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: spend.prevOut,
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(spend.amount),
|
|
BlockHeight: spend.blockHeight,
|
|
BlockIndex: spend.blockIndex,
|
|
SignatureScript: opTrueRedeemScript,
|
|
})
|
|
spendTx.AddTxOut(wire.NewTxOut(int64(spend.amount-fee),
|
|
g.p2shOpTrueScript))
|
|
spendTx.AddTxOut(wire.NewTxOut(0, UniqueOpReturnScript()))
|
|
return spendTx
|
|
}
|
|
|
|
// CreateSpendTxForTx creates a transaction that spends from the first output of
|
|
// the provided transaction and includes an additional unique OP_RETURN output
|
|
// to ensure the transaction ends up with a unique hash. The public key script
|
|
// is a simple OP_TRUE p2sh script which avoids the need to track addresses and
|
|
// signature scripts in the tests. This signature script the
|
|
// opTrueRedeemScript.
|
|
func (g *Generator) CreateSpendTxForTx(tx *wire.MsgTx, blockHeight, txIndex uint32, fee dcrutil.Amount) *wire.MsgTx {
|
|
spend := MakeSpendableOutForTx(tx, blockHeight, txIndex, 0)
|
|
return g.CreateSpendTx(&spend, fee)
|
|
}
|
|
|
|
// removeTicket removes the passed index from the provided slice of tickets and
|
|
// returns the resulting slice. This is an in-place modification.
|
|
func removeTicket(tickets []*stakeTicket, index int) []*stakeTicket {
|
|
copy(tickets[index:], tickets[index+1:])
|
|
tickets[len(tickets)-1] = nil // Prevent memory leak
|
|
tickets = tickets[:len(tickets)-1]
|
|
return tickets
|
|
}
|
|
|
|
// connectLiveTickets updates the live ticket pool for a new tip block by
|
|
// removing tickets that are now expired from it, removing the passed winners
|
|
// from it, adding any immature tickets which are now mature to it, and
|
|
// resorting it.
|
|
func (g *Generator) connectLiveTickets(blockHash *chainhash.Hash, height uint32, winners, purchases []*stakeTicket) {
|
|
// Move expired tickets from the live ticket pool to the expired ticket
|
|
// pool.
|
|
ticketMaturity := uint32(g.params.TicketMaturity)
|
|
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--
|
|
}
|
|
}
|
|
|
|
// Move winning tickets from the live ticket pool to won tickets pool.
|
|
for i := 0; i < len(g.liveTickets); i++ {
|
|
ticket := g.liveTickets[i]
|
|
for _, winner := range winners {
|
|
if ticket.tx.CachedTxHash() == winner.tx.CachedTxHash() {
|
|
g.liveTickets = removeTicket(g.liveTickets, i)
|
|
|
|
// 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--
|
|
break
|
|
}
|
|
}
|
|
}
|
|
g.wonTickets[*blockHash] = winners
|
|
|
|
// Move immature tickets which are now mature to the live ticket pool.
|
|
for i := 0; i < len(g.immatureTickets); i++ {
|
|
ticket := g.immatureTickets[i]
|
|
liveHeight := ticket.blockHeight + ticketMaturity
|
|
if height >= liveHeight {
|
|
g.immatureTickets = removeTicket(g.immatureTickets, i)
|
|
g.liveTickets = append(g.liveTickets, 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--
|
|
}
|
|
}
|
|
|
|
// Resort the ticket pool now that all live ticket pool manipulations
|
|
// are done.
|
|
sort.Sort(stakeTicketSorter(g.liveTickets))
|
|
|
|
// Add new ticket purchases to the immature ticket pool.
|
|
g.immatureTickets = append(g.immatureTickets, purchases...)
|
|
}
|
|
|
|
// addMissedVotes adds any of the passed winning tickets as missed votes if the
|
|
// passed block does not cast those votes.
|
|
func (g *Generator) addMissedVotes(blockHash *chainhash.Hash, stakeTxns []*wire.MsgTx, winners []*stakeTicket) {
|
|
// Nothing to do before there are any winning tickets.
|
|
if len(winners) == 0 {
|
|
return
|
|
}
|
|
|
|
// Assume all of the winning tickets were missed.
|
|
missedVotes := make(map[chainhash.Hash]*stakeTicket)
|
|
for _, ticket := range winners {
|
|
missedVotes[ticket.tx.TxHash()] = ticket
|
|
}
|
|
|
|
// Remove the entries for which the block actually contains votes.
|
|
for _, stx := range stakeTxns {
|
|
// Ignore all stake transactions that are not votes.
|
|
if !isVoteTx(stx) {
|
|
continue
|
|
}
|
|
|
|
// Ignore the vote if it is not for one of the winning tickets.
|
|
ticketInput := stx.TxIn[1]
|
|
ticketHash := ticketInput.PreviousOutPoint.Hash
|
|
missedVote, ok := missedVotes[ticketHash]
|
|
if !ok || missedVote.blockHeight != ticketInput.BlockHeight ||
|
|
missedVote.blockIndex != ticketInput.BlockIndex {
|
|
|
|
continue
|
|
}
|
|
|
|
delete(missedVotes, ticketHash)
|
|
}
|
|
|
|
// Add the missed votes to the generator state so future blocks will
|
|
// generate revocations for them.
|
|
for ticketHash, missedVote := range missedVotes {
|
|
g.missedVotes[ticketHash] = missedVote
|
|
}
|
|
}
|
|
|
|
// connectRevocations updates the missed and revoked ticket data structs
|
|
// according to the revocations in the passed block.
|
|
func (g *Generator) connectRevocations(blockHash *chainhash.Hash, stakeTxns []*wire.MsgTx) {
|
|
for _, stx := range stakeTxns {
|
|
// Ignore all stake transactions that are not revocations.
|
|
if !isRevocationTx(stx) {
|
|
continue
|
|
}
|
|
|
|
// Ignore the revocation if it is not for a missed ticket.
|
|
ticketInput := stx.TxIn[0]
|
|
ticketHash := ticketInput.PreviousOutPoint.Hash
|
|
ticket, ok := g.missedVotes[ticketHash]
|
|
if !ok || ticket.blockHeight != ticketInput.BlockHeight ||
|
|
ticket.blockIndex != ticketInput.BlockIndex {
|
|
|
|
continue
|
|
}
|
|
|
|
// Remove the revoked ticket from the missed votes and add it to the
|
|
// list of tickets revoked by the block.
|
|
delete(g.missedVotes, ticketHash)
|
|
g.revokedTickets[*blockHash] = append(g.revokedTickets[*blockHash],
|
|
ticket)
|
|
}
|
|
}
|
|
|
|
// connectBlockTickets updates the live ticket pool and associated data structs
|
|
// by for the passed block. It will panic if the specified block does not
|
|
// connect to the current tip block.
|
|
func (g *Generator) connectBlockTickets(b *wire.MsgBlock) {
|
|
// Attempt to prevent misuse of this function by ensuring the provided
|
|
// block connects to the current tip.
|
|
blockHash := b.BlockHash()
|
|
if b.Header.PrevBlock != g.tip.BlockHash() {
|
|
panic(fmt.Sprintf("attempt to connect block %s with parent %s "+
|
|
"that is not the current tip %s", blockHash,
|
|
b.Header.PrevBlock, g.tip.BlockHash()))
|
|
}
|
|
|
|
// Get all of the winning tickets for the block.
|
|
numVotes := g.params.TicketsPerBlock
|
|
winners, _, err := winningTickets(g.tip, g.liveTickets, numVotes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Keep track of any missed votes.
|
|
g.addMissedVotes(&blockHash, b.STransactions, winners)
|
|
|
|
// Keep track of revocations.
|
|
g.connectRevocations(&blockHash, b.STransactions)
|
|
|
|
// Extract the ticket purchases (sstx) from the block.
|
|
var purchases []*stakeTicket
|
|
blockHeight := g.blockHeight(blockHash)
|
|
for txIdx, tx := range b.STransactions {
|
|
if isTicketPurchaseTx(tx) {
|
|
ticket := &stakeTicket{tx, blockHeight, uint32(txIdx)}
|
|
purchases = append(purchases, ticket)
|
|
}
|
|
}
|
|
|
|
// Update the live ticket pool and associated data structures.
|
|
g.connectLiveTickets(&blockHash, blockHeight, winners, purchases)
|
|
}
|
|
|
|
// disconnectBlockTickets updates the live ticket pool and associated data
|
|
// structs by unwinding the passed block, which must be the current tip block.
|
|
// It will panic if the specified block is not the current tip block.
|
|
func (g *Generator) disconnectBlockTickets(b *wire.MsgBlock) {
|
|
// Attempt to prevent misuse of this function by ensuring the provided
|
|
// block is the current tip.
|
|
blockHash := b.BlockHash()
|
|
if b != g.tip {
|
|
panic(fmt.Sprintf("attempt to disconnect block %s that is not "+
|
|
"the current tip %s", blockHash, g.tip.BlockHash()))
|
|
}
|
|
|
|
// Move tickets revoked by the block back to the list of missed tickets.
|
|
for _, ticket := range g.revokedTickets[blockHash] {
|
|
g.missedVotes[ticket.tx.TxHash()] = ticket
|
|
}
|
|
|
|
// Remove any votes missed by the block.
|
|
winners := g.wonTickets[blockHash]
|
|
for _, ticket := range winners {
|
|
delete(g.missedVotes, ticket.tx.TxHash())
|
|
}
|
|
|
|
// Remove tickets created in the block from the immature ticket pool.
|
|
blockHeight := g.blockHeight(blockHash)
|
|
for i := 0; i < len(g.immatureTickets); i++ {
|
|
ticket := g.immatureTickets[i]
|
|
if ticket.blockHeight == blockHeight {
|
|
g.immatureTickets = removeTicket(g.immatureTickets, i)
|
|
|
|
// 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--
|
|
}
|
|
}
|
|
|
|
// Move tickets that are no longer mature from the live ticket pool to
|
|
// the immature ticket pool.
|
|
prevBlockHeight := blockHeight - 1
|
|
ticketMaturity := uint32(g.params.TicketMaturity)
|
|
for i := 0; i < len(g.liveTickets); i++ {
|
|
ticket := g.liveTickets[i]
|
|
liveHeight := ticket.blockHeight + ticketMaturity
|
|
if prevBlockHeight < liveHeight {
|
|
g.liveTickets = removeTicket(g.liveTickets, i)
|
|
g.immatureTickets = append(g.immatureTickets, 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--
|
|
}
|
|
}
|
|
|
|
// Move tickets that are no longer expired from the expired ticket pool
|
|
// to the live ticket pool.
|
|
ticketExpiry := g.params.TicketExpiry
|
|
for i := 0; i < len(g.expiredTickets); i++ {
|
|
ticket := g.expiredTickets[i]
|
|
liveHeight := ticket.blockHeight + ticketMaturity
|
|
expireHeight := liveHeight + ticketExpiry
|
|
if prevBlockHeight < expireHeight {
|
|
g.expiredTickets = removeTicket(g.expiredTickets, i)
|
|
g.liveTickets = append(g.liveTickets, 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--
|
|
}
|
|
}
|
|
|
|
// Add the winning tickets consumed by the block back to the live ticket
|
|
// pool.
|
|
g.liveTickets = append(g.liveTickets, winners...)
|
|
delete(g.wonTickets, blockHash)
|
|
|
|
// Resort the ticket pool now that all live ticket pool manipulations
|
|
// are done.
|
|
sort.Sort(stakeTicketSorter(g.liveTickets))
|
|
}
|
|
|
|
// originalParent returns the original block the passed block was built from.
|
|
// This is necessary because callers might change the previous block hash in a
|
|
// munger which would cause the like ticket pool to be reconstructed improperly.
|
|
func (g *Generator) originalParent(b *wire.MsgBlock) *wire.MsgBlock {
|
|
parentHash, ok := g.originalParents[b.BlockHash()]
|
|
if !ok {
|
|
parentHash = b.Header.PrevBlock
|
|
}
|
|
return g.BlockByHash(&parentHash)
|
|
}
|
|
|
|
// SetTip changes the tip of the instance to the block with the provided name.
|
|
// This is useful since the tip is used for things such as generating subsequent
|
|
// blocks.
|
|
func (g *Generator) SetTip(blockName string) {
|
|
// Nothing to do if already the tip.
|
|
if blockName == g.tipName {
|
|
return
|
|
}
|
|
|
|
newTip := g.blocksByName[blockName]
|
|
if newTip == nil {
|
|
panic(fmt.Sprintf("tip block name %s does not exist", blockName))
|
|
}
|
|
|
|
// Create a list of blocks to disconnect and blocks to connect in order
|
|
// to switch to the new tip.
|
|
var connect, disconnect []*wire.MsgBlock
|
|
oldBranch, newBranch := g.tip, newTip
|
|
for oldBranch != newBranch {
|
|
oldBranchHeight := g.blockHeight(oldBranch.BlockHash())
|
|
newBranchHeight := g.blockHeight(newBranch.BlockHash())
|
|
if oldBranchHeight > newBranchHeight {
|
|
disconnect = append(disconnect, oldBranch)
|
|
oldBranch = g.originalParent(oldBranch)
|
|
continue
|
|
} else if newBranchHeight > oldBranchHeight {
|
|
connect = append(connect, newBranch)
|
|
newBranch = g.originalParent(newBranch)
|
|
continue
|
|
}
|
|
|
|
// At this point the two branches have the same height, so add
|
|
// each tip to the appropriate connect or disconnect list and
|
|
// the tips to their previous block.
|
|
disconnect = append(disconnect, oldBranch)
|
|
oldBranch = g.originalParent(oldBranch)
|
|
connect = append(connect, newBranch)
|
|
newBranch = g.originalParent(newBranch)
|
|
}
|
|
|
|
// Update the live ticket pool and associated data structs by
|
|
// disconnecting all blocks back to the fork point.
|
|
for _, block := range disconnect {
|
|
g.disconnectBlockTickets(block)
|
|
g.tip = g.originalParent(block)
|
|
}
|
|
|
|
// Update the live ticket pool and associated data structs by connecting
|
|
// all blocks after the fork point up to the new tip. The list of
|
|
// blocks to connect is iterated in reverse order, because it was
|
|
// constructed in reverse, and the blocks need to be connected in the
|
|
// order in which they build the chain.
|
|
for i := len(connect) - 1; i >= 0; i-- {
|
|
block := connect[i]
|
|
g.connectBlockTickets(block)
|
|
g.tip = block
|
|
}
|
|
|
|
// Ensure the tip is the expected new tip and set the associated name.
|
|
if g.tip != newTip {
|
|
panic(fmt.Sprintf("tip %s is not expected new tip %s",
|
|
g.tip.BlockHash(), newTip.BlockHash()))
|
|
}
|
|
g.tipName = blockName
|
|
}
|
|
|
|
// updateVoteCommitments updates all of the votes in the passed block to commit
|
|
// to the previous block hash and previous height based on the values specified
|
|
// in the header.
|
|
func updateVoteCommitments(block *wire.MsgBlock) {
|
|
for _, stx := range block.STransactions {
|
|
if !isVoteTx(stx) {
|
|
continue
|
|
}
|
|
|
|
stx.TxOut[0].PkScript = VoteCommitmentScript(block.Header.PrevBlock,
|
|
block.Header.Height-1)
|
|
}
|
|
}
|
|
|
|
// NextBlock builds a new block that extends the current tip associated with the
|
|
// generator and updates the generator's tip to the newly generated block.
|
|
//
|
|
// The block will include the following:
|
|
// - A coinbase with the following outputs:
|
|
// - One that pays the required 10% subsidy to the dev org
|
|
// - One that contains a standard coinbase OP_RETURN script
|
|
// - Six that pay the required 60% subsidy to an OP_TRUE p2sh script
|
|
// - When a spendable output is provided:
|
|
// - A transaction that spends from the provided output the following outputs:
|
|
// - One that pays the inputs amount minus 1 atom to an OP_TRUE p2sh script
|
|
// - Once the coinbase maturity has been reached:
|
|
// - A ticket purchase transaction (sstx) for each provided ticket spendable
|
|
// output with the following outputs:
|
|
// - One OP_SSTX output that grants voting rights to an OP_TRUE p2sh script
|
|
// - One OP_RETURN output that contains the required commitment and pays
|
|
// the subsidy to an OP_TRUE p2sh script
|
|
// - One OP_SSTXCHANGE output that sends change to an OP_TRUE p2sh script
|
|
// - Once the stake validation height has been reached:
|
|
// - 5 vote transactions (ssgen) as required according to the live ticket
|
|
// pool and vote selection rules with the following outputs:
|
|
// - One OP_RETURN followed by the block hash and height being voted on
|
|
// - One OP_RETURN followed by the vote bits
|
|
// - One or more OP_SSGEN outputs with the payouts according to the original
|
|
// ticket commitments
|
|
// - Revocation transactions (ssrtx) as required according to any missed votes
|
|
// with the following outputs:
|
|
// - One or more OP_SSRTX outputs with the payouts according to the original
|
|
// ticket commitments
|
|
//
|
|
// Additionally, if one or more munge functions are specified, they will be
|
|
// invoked with the block prior to solving it. This provides callers with the
|
|
// opportunity to modify the block which is especially useful for testing.
|
|
//
|
|
// In order to simply the logic in the munge functions, the following rules are
|
|
// applied after all munge functions have been invoked:
|
|
// - All votes will have their commitments updated if the previous hash or
|
|
// height was manually changed after stake validation height has been reached
|
|
// - The merkle root will be recalculated unless it was manually changed
|
|
// - The stake root will be recalculated unless it was manually changed
|
|
// - The size of the block will be recalculated unless it was manually changed
|
|
// - The block will be solved unless the nonce was changed
|
|
func (g *Generator) NextBlock(blockName string, spend *SpendableOut, ticketSpends []SpendableOut, mungers ...func(*wire.MsgBlock)) *wire.MsgBlock {
|
|
// Prevent block name collisions.
|
|
if g.blocksByName[blockName] != nil {
|
|
panic(fmt.Sprintf("block name %s already exists", blockName))
|
|
}
|
|
|
|
// Calculate the next required stake difficulty (aka ticket price).
|
|
ticketPrice := dcrutil.Amount(g.CalcNextRequiredStakeDifficulty())
|
|
|
|
// Generate the appropriate votes and ticket purchases based on the
|
|
// current tip block and provided ticket spendable outputs.
|
|
var ticketWinners []*stakeTicket
|
|
var stakeTxns []*wire.MsgTx
|
|
var finalState [6]byte
|
|
nextHeight := g.tip.Header.Height + 1
|
|
if nextHeight > uint32(g.params.CoinbaseMaturity) {
|
|
// Generate votes once the stake validation height has been
|
|
// reached.
|
|
if int64(nextHeight) >= g.params.StakeValidationHeight {
|
|
// Generate and add the vote transactions for the
|
|
// winning tickets to the stake tree.
|
|
numVotes := g.params.TicketsPerBlock
|
|
winners, stateHash, err := winningTickets(g.tip,
|
|
g.liveTickets, numVotes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ticketWinners = winners
|
|
for _, ticket := range winners {
|
|
voteTx := g.createVoteTxFromTicket(g.tip, ticket)
|
|
stakeTxns = append(stakeTxns, voteTx)
|
|
}
|
|
|
|
// Calculate the final lottery state hash for use in the
|
|
// block header.
|
|
finalState = calcFinalLotteryState(winners, stateHash)
|
|
}
|
|
|
|
// Generate ticket purchases (sstx) using the provided spendable
|
|
// outputs.
|
|
if ticketSpends != nil {
|
|
const ticketFee = dcrutil.Amount(2)
|
|
for i := 0; i < len(ticketSpends); i++ {
|
|
out := &ticketSpends[i]
|
|
purchaseTx := g.CreateTicketPurchaseTx(out,
|
|
ticketPrice, ticketFee)
|
|
stakeTxns = append(stakeTxns, purchaseTx)
|
|
}
|
|
}
|
|
|
|
// Generate and add revocations for any missed tickets.
|
|
for _, missedVote := range g.missedVotes {
|
|
revocationTx := g.createRevocationTxFromTicket(missedVote)
|
|
stakeTxns = append(stakeTxns, revocationTx)
|
|
}
|
|
}
|
|
|
|
// Count the ticket purchases (sstx), votes (ssgen, and ticket revocations
|
|
// (ssrtx), and calculate the total PoW fees generated by the stake
|
|
// transactions.
|
|
var numVotes uint16
|
|
var numTicketRevocations uint8
|
|
var numTicketPurchases uint8
|
|
var stakeTreeFees dcrutil.Amount
|
|
for _, tx := range stakeTxns {
|
|
switch {
|
|
case isVoteTx(tx):
|
|
numVotes++
|
|
case isTicketPurchaseTx(tx):
|
|
numTicketPurchases++
|
|
case isRevocationTx(tx):
|
|
numTicketRevocations++
|
|
}
|
|
|
|
// Calculate any fees for the transaction.
|
|
var inputSum, outputSum dcrutil.Amount
|
|
for _, txIn := range tx.TxIn {
|
|
inputSum += dcrutil.Amount(txIn.ValueIn)
|
|
}
|
|
for _, txOut := range tx.TxOut {
|
|
outputSum += dcrutil.Amount(txOut.Value)
|
|
}
|
|
stakeTreeFees += (inputSum - outputSum)
|
|
}
|
|
|
|
// Create a standard coinbase and spending transaction.
|
|
var regularTxns []*wire.MsgTx
|
|
{
|
|
// Create coinbase transaction for the block with no additional
|
|
// dev or pow subsidy.
|
|
coinbaseTx := g.CreateCoinbaseTx(nextHeight, numVotes)
|
|
regularTxns = []*wire.MsgTx{coinbaseTx}
|
|
|
|
// Increase the PoW subsidy to account for any fees in the stake
|
|
// tree.
|
|
coinbaseTx.TxOut[2].Value += int64(stakeTreeFees)
|
|
|
|
// Create a transaction to spend the provided utxo if needed.
|
|
if spend != nil {
|
|
// Create the transaction with a fee of 1 atom for the
|
|
// miner and increase the PoW subsidy accordingly.
|
|
fee := dcrutil.Amount(1)
|
|
coinbaseTx.TxOut[2].Value += int64(fee)
|
|
|
|
// Create a transaction that spends from the provided
|
|
// spendable output and includes an additional unique
|
|
// OP_RETURN output to ensure the transaction ends up
|
|
// with a unique hash, then add it to the list of
|
|
// transactions to include in the block. The script is
|
|
// a simple OP_TRUE p2sh script in order to avoid the
|
|
// need to track addresses and signature scripts in the
|
|
// tests.
|
|
spendTx := g.CreateSpendTx(spend, fee)
|
|
regularTxns = append(regularTxns, spendTx)
|
|
}
|
|
}
|
|
|
|
// Use a timestamp that is 7/8 of target timespan after the previous
|
|
// block unless this is the first block in which case the current time
|
|
// is used or the proof-of-work difficulty parameters have been adjusted
|
|
// such that it's greater than the max 2 hours worth of blocks that can
|
|
// be tested in which case one second is used. This helps maintain the
|
|
// retarget difficulty low as needed. Also, ensure the timestamp is
|
|
// limited to one second precision.
|
|
var ts time.Time
|
|
if nextHeight == 1 {
|
|
ts = time.Now()
|
|
} else {
|
|
if g.params.WorkDiffWindowSize > 7200 {
|
|
ts = g.tip.Header.Timestamp.Add(time.Second)
|
|
} else {
|
|
addDuration := g.params.TargetTimespan * 7 / 8
|
|
ts = g.tip.Header.Timestamp.Add(addDuration)
|
|
}
|
|
}
|
|
ts = time.Unix(ts.Unix(), 0)
|
|
|
|
// Create the unsolved block.
|
|
prevHash := g.tip.BlockHash()
|
|
block := wire.MsgBlock{
|
|
Header: wire.BlockHeader{
|
|
Version: 1,
|
|
PrevBlock: prevHash,
|
|
MerkleRoot: calcMerkleRoot(regularTxns),
|
|
StakeRoot: calcMerkleRoot(stakeTxns),
|
|
VoteBits: 1,
|
|
FinalState: finalState,
|
|
Voters: numVotes,
|
|
FreshStake: numTicketPurchases,
|
|
Revocations: numTicketRevocations,
|
|
PoolSize: uint32(len(g.liveTickets)),
|
|
Bits: g.CalcNextRequiredDifficulty(),
|
|
SBits: int64(ticketPrice),
|
|
Height: nextHeight,
|
|
Size: 0, // Filled in below.
|
|
Timestamp: ts,
|
|
Nonce: 0, // To be solved.
|
|
ExtraData: [32]byte{},
|
|
StakeVersion: 0,
|
|
},
|
|
Transactions: regularTxns,
|
|
STransactions: stakeTxns,
|
|
}
|
|
block.Header.Size = uint32(block.SerializeSize())
|
|
|
|
// Perform any block munging just before solving. Once stake validation
|
|
// height has been reached, update the vote commitments accordingly if the
|
|
// header height or previous hash was manually changed by a munge function.
|
|
// Also, only recalculate the merkle roots and block size if they weren't
|
|
// manually changed by a munge function.
|
|
curMerkleRoot := block.Header.MerkleRoot
|
|
curStakeRoot := block.Header.StakeRoot
|
|
curSize := block.Header.Size
|
|
curNonce := block.Header.Nonce
|
|
for _, f := range mungers {
|
|
f(&block)
|
|
}
|
|
if block.Header.Height != nextHeight || block.Header.PrevBlock != prevHash {
|
|
if int64(nextHeight) >= g.params.StakeValidationHeight {
|
|
updateVoteCommitments(&block)
|
|
}
|
|
}
|
|
if block.Header.MerkleRoot == curMerkleRoot {
|
|
block.Header.MerkleRoot = calcMerkleRoot(block.Transactions)
|
|
}
|
|
if block.Header.StakeRoot == curStakeRoot {
|
|
block.Header.StakeRoot = calcMerkleRoot(block.STransactions)
|
|
}
|
|
if block.Header.Size == curSize {
|
|
block.Header.Size = uint32(block.SerializeSize())
|
|
}
|
|
|
|
// Only solve the block if the nonce wasn't manually changed by a munge
|
|
// function.
|
|
if block.Header.Nonce == curNonce && !solveBlock(&block.Header) {
|
|
panic(fmt.Sprintf("unable to solve block at height %d",
|
|
block.Header.Height))
|
|
}
|
|
|
|
// Create stake tickets for the ticket purchases (sstx) in the block. This
|
|
// is done after the mungers to ensure all changes are accurately accounted
|
|
// for.
|
|
var ticketPurchases []*stakeTicket
|
|
for txIdx, tx := range block.STransactions {
|
|
if isTicketPurchaseTx(tx) {
|
|
ticket := &stakeTicket{tx, nextHeight, uint32(txIdx)}
|
|
ticketPurchases = append(ticketPurchases, ticket)
|
|
}
|
|
}
|
|
|
|
// Update generator state and return the block.
|
|
blockHash := block.BlockHash()
|
|
if block.Header.PrevBlock != prevHash {
|
|
// Save the original block this one was built from if it was
|
|
// manually changed in a munger so the code which deals with
|
|
// updating the live tickets when changing the tip has access to
|
|
// it.
|
|
g.originalParents[blockHash] = prevHash
|
|
}
|
|
g.addMissedVotes(&blockHash, block.STransactions, ticketWinners)
|
|
g.connectRevocations(&blockHash, block.STransactions)
|
|
g.connectLiveTickets(&blockHash, nextHeight, ticketWinners,
|
|
ticketPurchases)
|
|
g.blocks[blockHash] = &block
|
|
g.blockHeights[blockHash] = nextHeight
|
|
g.blocksByName[blockName] = &block
|
|
g.tip = &block
|
|
g.tipName = blockName
|
|
return &block
|
|
}
|
|
|
|
// CreateBlockOne generates the first block of the chain with the required
|
|
// payouts. The additional amount parameter can be used to create a block that
|
|
// is otherwise a completely valid block one except it adds the extra amount to
|
|
// each payout and thus create a block that violates consensus.
|
|
func (g *Generator) CreateBlockOne(blockName string, additionalAmount dcrutil.Amount, mungers ...func(*wire.MsgBlock)) *wire.MsgBlock {
|
|
coinbaseTx := wire.NewMsgTx()
|
|
coinbaseTx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
|
|
wire.MaxPrevOutIndex, wire.TxTreeRegular),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: 0, // Updated below.
|
|
BlockHeight: wire.NullBlockHeight,
|
|
BlockIndex: wire.NullBlockIndex,
|
|
SignatureScript: coinbaseSigScript,
|
|
})
|
|
|
|
// Add each required output and tally the total payouts for the coinbase
|
|
// in order to set the input value appropriately.
|
|
var totalSubsidy dcrutil.Amount
|
|
for _, payout := range g.params.BlockOneLedger {
|
|
coinbaseTx.AddTxOut(&wire.TxOut{
|
|
Value: payout.Amount + int64(additionalAmount),
|
|
Version: payout.ScriptVersion,
|
|
PkScript: payout.Script,
|
|
})
|
|
|
|
totalSubsidy += dcrutil.Amount(payout.Amount)
|
|
}
|
|
coinbaseTx.TxIn[0].ValueIn = int64(totalSubsidy)
|
|
|
|
// Generate the block with the specially created regular transactions.
|
|
munger := func(b *wire.MsgBlock) {
|
|
b.Transactions = []*wire.MsgTx{coinbaseTx}
|
|
}
|
|
mungers = append([]func(*wire.MsgBlock){munger}, mungers...)
|
|
return g.NextBlock(blockName, nil, nil, mungers...)
|
|
}
|
|
|
|
// UpdateBlockState manually updates the generator state to remove all internal
|
|
// map references to a block via its old hash and insert new ones for the new
|
|
// block hash. This is useful if the test code has to manually change a block
|
|
// after 'NextBlock' has returned.
|
|
func (g *Generator) UpdateBlockState(oldBlockName string, oldBlockHash chainhash.Hash, newBlockName string, newBlock *wire.MsgBlock) {
|
|
// Remove existing entries.
|
|
wonTickets := g.wonTickets[oldBlockHash]
|
|
existingHeight := g.blockHeights[oldBlockHash]
|
|
delete(g.blocks, oldBlockHash)
|
|
delete(g.blockHeights, oldBlockHash)
|
|
delete(g.blocksByName, oldBlockName)
|
|
delete(g.wonTickets, oldBlockHash)
|
|
|
|
// Add new entries.
|
|
newBlockHash := newBlock.BlockHash()
|
|
g.blocks[newBlockHash] = newBlock
|
|
g.blockHeights[newBlockHash] = existingHeight
|
|
g.blocksByName[newBlockName] = newBlock
|
|
g.wonTickets[newBlockHash] = wonTickets
|
|
}
|
|
|
|
// OldestCoinbaseOuts removes the oldest set of coinbase proof-of-work outputs
|
|
// that was previously saved to the generator and returns the set as a slice.
|
|
func (g *Generator) OldestCoinbaseOuts() []SpendableOut {
|
|
outs := g.spendableOuts[0]
|
|
g.spendableOuts = g.spendableOuts[1:]
|
|
return outs
|
|
}
|
|
|
|
// NumSpendableCoinbaseOuts returns the number of proof-of-work outputs that
|
|
// were previously saved to the generated but have not yet been collected.
|
|
func (g *Generator) NumSpendableCoinbaseOuts() int {
|
|
return len(g.spendableOuts)
|
|
}
|
|
|
|
// saveCoinbaseOuts adds the proof-of-work outputs of the coinbase tx in the
|
|
// passed block to the list of spendable outputs.
|
|
func (g *Generator) saveCoinbaseOuts(b *wire.MsgBlock) {
|
|
g.spendableOuts = append(g.spendableOuts, []SpendableOut{
|
|
MakeSpendableOut(b, 0, 2),
|
|
MakeSpendableOut(b, 0, 3),
|
|
MakeSpendableOut(b, 0, 4),
|
|
MakeSpendableOut(b, 0, 5),
|
|
MakeSpendableOut(b, 0, 6),
|
|
MakeSpendableOut(b, 0, 7),
|
|
})
|
|
g.prevCollectedHash = b.BlockHash()
|
|
}
|
|
|
|
// SaveTipCoinbaseOuts adds the proof-of-work outputs of the coinbase tx in the
|
|
// current tip block to the list of spendable outputs.
|
|
func (g *Generator) SaveTipCoinbaseOuts() {
|
|
g.saveCoinbaseOuts(g.tip)
|
|
}
|
|
|
|
// SaveSpendableCoinbaseOuts adds all proof-of-work coinbase outputs starting
|
|
// from the block after the last block that had its coinbase outputs collected
|
|
// and ending at the current tip. This is useful to batch the collection of the
|
|
// outputs once the tests reach a stable point so they don't have to manually
|
|
// add them for the right tests which will ultimately end up being the best
|
|
// chain.
|
|
func (g *Generator) SaveSpendableCoinbaseOuts() {
|
|
// Loop through the ancestors of the current tip until the
|
|
// reaching the block that has already had the coinbase outputs
|
|
// collected.
|
|
var collectBlocks []*wire.MsgBlock
|
|
for b := g.tip; b != nil; b = g.blocks[b.Header.PrevBlock] {
|
|
if b.BlockHash() == g.prevCollectedHash {
|
|
break
|
|
}
|
|
collectBlocks = append(collectBlocks, b)
|
|
}
|
|
for i := range collectBlocks {
|
|
g.saveCoinbaseOuts(collectBlocks[len(collectBlocks)-1-i])
|
|
}
|
|
}
|
|
|
|
// AssertTipHeight panics if the current tip block associated with the generator
|
|
// does not have the specified height.
|
|
func (g *Generator) AssertTipHeight(expected uint32) {
|
|
height := g.tip.Header.Height
|
|
if height != expected {
|
|
panic(fmt.Sprintf("height for block %q is %d instead of "+
|
|
"expected %d", g.tipName, height, expected))
|
|
}
|
|
}
|
|
|
|
// AssertScriptSigOpsCount panics if the provided script does not have the
|
|
// specified number of signature operations.
|
|
func (g *Generator) AssertScriptSigOpsCount(script []byte, expected int) {
|
|
numSigOps := txscript.GetSigOpCount(script)
|
|
if numSigOps != expected {
|
|
_, file, line, _ := runtime.Caller(1)
|
|
panic(fmt.Sprintf("assertion failed at %s:%d: generated number "+
|
|
"of sigops for script is %d instead of expected %d",
|
|
file, line, numSigOps, expected))
|
|
}
|
|
}
|
|
|
|
// countBlockSigOps returns the number of legacy signature operations in the
|
|
// scripts in the passed block.
|
|
func countBlockSigOps(block *wire.MsgBlock) int {
|
|
totalSigOps := 0
|
|
for _, tx := range block.Transactions {
|
|
for _, txIn := range tx.TxIn {
|
|
numSigOps := txscript.GetSigOpCount(txIn.SignatureScript)
|
|
totalSigOps += numSigOps
|
|
}
|
|
for _, txOut := range tx.TxOut {
|
|
numSigOps := txscript.GetSigOpCount(txOut.PkScript)
|
|
totalSigOps += numSigOps
|
|
}
|
|
}
|
|
|
|
return totalSigOps
|
|
}
|
|
|
|
// AssertTipBlockSigOpsCount panics if the current tip block associated with the
|
|
// generator does not have the specified number of signature operations.
|
|
func (g *Generator) AssertTipBlockSigOpsCount(expected int) {
|
|
numSigOps := countBlockSigOps(g.tip)
|
|
if numSigOps != expected {
|
|
panic(fmt.Sprintf("generated number of sigops for block %q "+
|
|
"(height %d) is %d instead of expected %d", g.tipName,
|
|
g.tip.Header.Height, numSigOps, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipBlockSize panics if the current tip block associated with the
|
|
// generator does not have the specified size when serialized.
|
|
func (g *Generator) AssertTipBlockSize(expected int) {
|
|
serializeSize := g.tip.SerializeSize()
|
|
if serializeSize != expected {
|
|
panic(fmt.Sprintf("block size of block %q (height %d) is %d "+
|
|
"instead of expected %d", g.tipName,
|
|
g.tip.Header.Height, serializeSize, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipBlockNumTxns panics if the number of transactions in the current tip
|
|
// block associated with the generator does not match the specified value.
|
|
func (g *Generator) AssertTipBlockNumTxns(expected int) {
|
|
numTxns := len(g.tip.Transactions)
|
|
if numTxns != expected {
|
|
panic(fmt.Sprintf("number of txns in block %q (height %d) is "+
|
|
"%d instead of expected %d", g.tipName,
|
|
g.tip.Header.Height, numTxns, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipBlockHash panics if the current tip block associated with the
|
|
// generator does not match the specified hash.
|
|
func (g *Generator) AssertTipBlockHash(expected chainhash.Hash) {
|
|
hash := g.tip.BlockHash()
|
|
if hash != expected {
|
|
panic(fmt.Sprintf("block hash of block %q (height %d) is %v "+
|
|
"instead of expected %v", g.tipName,
|
|
g.tip.Header.Height, hash, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipBlockMerkleRoot panics if the merkle root in header of the current
|
|
// tip block associated with the generator does not match the specified hash.
|
|
func (g *Generator) AssertTipBlockMerkleRoot(expected chainhash.Hash) {
|
|
hash := g.tip.Header.MerkleRoot
|
|
if hash != expected {
|
|
panic(fmt.Sprintf("merkle root of block %q (height %d) is %v "+
|
|
"instead of expected %v", g.tipName,
|
|
g.tip.Header.Height, hash, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipBlockStakeRoot panics if the stake root in header of the current
|
|
// tip block associated with the generator does not match the specified hash.
|
|
func (g *Generator) AssertTipBlockStakeRoot(expected chainhash.Hash) {
|
|
hash := g.tip.Header.StakeRoot
|
|
if hash != expected {
|
|
panic(fmt.Sprintf("stake root of block %q (height %d) is %v "+
|
|
"instead of expected %v", g.tipName,
|
|
g.tip.Header.Height, hash, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipBlockTxOutOpReturn panics if the current tip block associated with
|
|
// the generator does not have an OP_RETURN script for the transaction output at
|
|
// the provided tx index and output index.
|
|
func (g *Generator) AssertTipBlockTxOutOpReturn(txIndex, txOutIndex uint32) {
|
|
if txIndex >= uint32(len(g.tip.Transactions)) {
|
|
panic(fmt.Sprintf("transaction index %d in block %q "+
|
|
"(height %d) does not exist", txIndex, g.tipName,
|
|
g.tip.Header.Height))
|
|
}
|
|
|
|
tx := g.tip.Transactions[txIndex]
|
|
if txOutIndex >= uint32(len(tx.TxOut)) {
|
|
panic(fmt.Sprintf("transaction index %d output %d in block %q "+
|
|
"(height %d) does not exist", txIndex, txOutIndex,
|
|
g.tipName, g.tip.Header.Height))
|
|
}
|
|
|
|
txOut := tx.TxOut[txOutIndex]
|
|
if txOut.PkScript[0] != txscript.OP_RETURN {
|
|
panic(fmt.Sprintf("transaction index %d output %d in block %q "+
|
|
"(height %d) is not an OP_RETURN", txIndex, txOutIndex,
|
|
g.tipName, g.tip.Header.Height))
|
|
}
|
|
}
|
|
|
|
// AssertStakeVersion panics if the current tip block associated with the
|
|
// generator does not have the specified stake version in the header.
|
|
func (g *Generator) AssertStakeVersion(expected uint32) {
|
|
stakeVersion := g.tip.Header.StakeVersion
|
|
if stakeVersion != expected {
|
|
panic(fmt.Sprintf("stake version for block %q is %d instead of "+
|
|
"expected %d", g.tipName, stakeVersion, expected))
|
|
}
|
|
}
|
|
|
|
// AssertBlockVersion panics if the current tip block associated with the
|
|
// generator does not have the specified block version in the header.
|
|
func (g *Generator) AssertBlockVersion(expected int32) {
|
|
blockVersion := g.tip.Header.Version
|
|
if blockVersion != expected {
|
|
panic(fmt.Sprintf("block version for block %q is %d instead of "+
|
|
"expected %d", g.tipName, blockVersion, expected))
|
|
}
|
|
}
|
|
|
|
// AssertPoolSize panics if the current tip block associated with the generator
|
|
// does not indicate the specified pool size.
|
|
func (g *Generator) AssertPoolSize(expected uint32) {
|
|
poolSize := g.tip.Header.PoolSize
|
|
if poolSize != expected {
|
|
panic(fmt.Sprintf("pool size for block %q is %d instead of expected "+
|
|
"%d", g.tipName, poolSize, expected))
|
|
}
|
|
}
|
|
|
|
// AssertBlockRevocationTx panics if the current tip block associated with the
|
|
// generator does not have a revocation at the specified transaction index
|
|
// provided.
|
|
func (g *Generator) AssertBlockRevocationTx(b *wire.MsgBlock, txIndex uint32) {
|
|
if !isRevocationTx(b.STransactions[txIndex]) {
|
|
panic(fmt.Sprintf("stake transaction at index %d in block %q is "+
|
|
" not a revocation", txIndex, g.tipName))
|
|
}
|
|
}
|
|
|
|
// AssertTipNumRevocations panics if the number of revocations in header of the
|
|
// current tip block associated with the generator does not match the specified
|
|
// value.
|
|
func (g *Generator) AssertTipNumRevocations(expected uint8) {
|
|
numRevocations := g.tip.Header.Revocations
|
|
if numRevocations != expected {
|
|
panic(fmt.Sprintf("number of revocations in block %q (height "+
|
|
"%d) is %d instead of expected %d", g.tipName,
|
|
g.tip.Header.Height, numRevocations, expected))
|
|
}
|
|
}
|
|
|
|
// AssertTipDisapprovesPrevious panics if the current tip block associated with
|
|
// the generator does not disapprove the previous block.
|
|
func (g *Generator) AssertTipDisapprovesPrevious() {
|
|
if g.tip.Header.VoteBits&voteBitYes == 1 {
|
|
panic(fmt.Sprintf("block %q (height %d) does not disapprove prev block",
|
|
g.tipName, g.tip.Header.Height))
|
|
}
|
|
}
|