This updates the various blockchain call sites that deal with determining if a node is an ancestor (or descendant) to make use of the new IsAncestorOf convenience func.
1113 lines
35 KiB
Go
1113 lines
35 KiB
Go
// Copyright (c) 2020 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package blockchain
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
|
"github.com/decred/dcrd/blockchain/standalone/v2"
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/database/v3"
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr"
|
|
"github.com/decred/dcrd/dcrutil/v4"
|
|
"github.com/decred/dcrd/txscript/v4"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
const (
|
|
// yesTreasury signifies the treasury agenda should be treated as
|
|
// though it is active. It is used to increase the readability of the
|
|
// code.
|
|
yesTreasury = true
|
|
)
|
|
|
|
// errDbTreasury signifies that a problem was encountered when fetching or
|
|
// writing the treasury balance for a given block.
|
|
type errDbTreasury string
|
|
|
|
// Error implements the error interface.
|
|
func (e errDbTreasury) Error() string {
|
|
return string(e)
|
|
}
|
|
|
|
// Is implements the interface to work with the standard library's errors.Is.
|
|
//
|
|
// It returns true in the following cases:
|
|
// - The target is errDbTreasury
|
|
func (e errDbTreasury) Is(target error) bool {
|
|
var err errDbTreasury
|
|
return errors.As(target, &err)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// The treasury state index consists of an entry for every known block. It
|
|
// contains the balance of the treasury as of that block as well as all of the
|
|
// amounts added to and spent from the treasury in the block.
|
|
//
|
|
// The serialized key format is:
|
|
//
|
|
// <block hash>
|
|
//
|
|
// Field Type Size
|
|
// block hash chainhash.Hash chainhash.HashSize
|
|
//
|
|
// The serialized value format is:
|
|
//
|
|
// <balance><num values><values info>
|
|
//
|
|
// Field Type Size
|
|
// balance VLQ variable
|
|
// num values VLQ variable
|
|
// values
|
|
// flag VLQ variable
|
|
// value VLQ variable
|
|
//
|
|
// The flag attribute of each value is a bit field, serialized as follows:
|
|
//
|
|
// 00000ttt
|
|
// | | mask
|
|
// | \---- 0x07: treasuryValueType
|
|
// \-------------- : Unused
|
|
//
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// treasuryValueType specifies the possible types of values that modify the
|
|
// treasury balance.
|
|
type treasuryValueType byte
|
|
|
|
// IsDebit returns true if the type of value is a debit from the treasury
|
|
// account.
|
|
func (typ treasuryValueType) IsDebit() bool {
|
|
return typ == treasuryValueFee || typ == treasuryValueTSpend
|
|
}
|
|
|
|
// The following constants define the known types of values that modify the
|
|
// treasury balance.
|
|
const (
|
|
treasuryValueTBase treasuryValueType = 0x01
|
|
treasuryValueTAdd treasuryValueType = 0x02
|
|
treasuryValueFee treasuryValueType = 0x03
|
|
treasuryValueTSpend treasuryValueType = 0x04
|
|
|
|
// tvFlagTypMask is the mask of the bits used to encode the type in the
|
|
// flags field of a serialized treasuryValue.
|
|
tvFlagTypMask byte = 0x07
|
|
)
|
|
|
|
// treasuryValue specifies the type and amount that of a value that changes the
|
|
// treasury balance.
|
|
//
|
|
// NOTE: for tspends and tspend fees, the amount is *negative*.
|
|
type treasuryValue struct {
|
|
typ treasuryValueType
|
|
amount int64
|
|
}
|
|
|
|
// treasuryState records the treasury balance as of this block as well as the
|
|
// yet to mature balance-changing values (treasurybase, adds, spends and spend
|
|
// fees) included in the block itself.
|
|
//
|
|
// Note that treasurybase and tadds are positive and tspends and tspend fees
|
|
// are negative. Additionally the values are written in the exact same order
|
|
// as they appear in the block. This can be used to verify the correctness of
|
|
// the record if needed.
|
|
type treasuryState struct {
|
|
// balance is the treasury balance as of this block.
|
|
balance int64
|
|
|
|
// values stores all balance-changing values included in this block (for use
|
|
// when block is mature).
|
|
values []treasuryValue
|
|
}
|
|
|
|
// serializeTreasuryState serializes the treasury state into a single byte slice
|
|
// according to the format described in detail above.
|
|
func serializeTreasuryState(ts treasuryState) ([]byte, error) {
|
|
// Just a little sanity testing.
|
|
if ts.balance < 0 {
|
|
str := fmt.Sprintf("invalid treasury balance: %v", ts.balance)
|
|
return nil, errDbTreasury(str)
|
|
}
|
|
|
|
// Calculate total number of bytes it will take to serialize the treasury
|
|
// state according to the format described above.
|
|
serializeSize := serializeSizeVLQ(uint64(ts.balance)) +
|
|
serializeSizeVLQ(uint64(len(ts.values)))
|
|
for _, value := range ts.values {
|
|
// Prevent serialization of wrong negative value. Note that
|
|
// zero is still allowed even in negative types.
|
|
wantNegative := value.typ.IsDebit()
|
|
gotNegative := value.amount < 0
|
|
if value.amount != 0 && wantNegative != gotNegative {
|
|
str := fmt.Sprintf("incorrect negative value for type "+
|
|
"%d: %d", value.typ, value.amount)
|
|
return nil, errDbTreasury(str)
|
|
}
|
|
|
|
serializeSize += 1 // Flag which is currently a 1 byte long VLQ.
|
|
serializeSize += serializeSizeVLQ(absInt64(value.amount))
|
|
}
|
|
|
|
// Serialize the treasury state according to the format described above.
|
|
serialized := make([]byte, serializeSize)
|
|
offset := putVLQ(serialized, uint64(ts.balance))
|
|
offset += putVLQ(serialized[offset:], uint64(len(ts.values)))
|
|
for _, value := range ts.values {
|
|
// tspends and fees are negative but store them as positive to
|
|
// reduce storage needs.
|
|
amount := absInt64(value.amount)
|
|
flag := uint64(byte(value.typ) & tvFlagTypMask)
|
|
|
|
offset += putVLQ(serialized[offset:], flag)
|
|
offset += putVLQ(serialized[offset:], amount)
|
|
}
|
|
return serialized, nil
|
|
}
|
|
|
|
// deserializeTreasuryState deserializes the passed serialized treasury state
|
|
// according to the format described above.
|
|
func deserializeTreasuryState(data []byte) (*treasuryState, error) {
|
|
// Deserialize the balance.
|
|
balance, offset := deserializeVLQ(data)
|
|
if offset == 0 {
|
|
return nil, errDeserialize("unexpected end of data while reading " +
|
|
"treasury balance")
|
|
}
|
|
|
|
// Deserialize the number of values.
|
|
var values []treasuryValue
|
|
numValues, bytesRead := deserializeVLQ(data[offset:])
|
|
if bytesRead == 0 {
|
|
return nil, errDeserialize("unexpected end of data while reading " +
|
|
"number of value entries")
|
|
}
|
|
offset += bytesRead
|
|
|
|
// Deserialize individual values.
|
|
if numValues > 0 {
|
|
values = make([]treasuryValue, numValues)
|
|
for i := uint64(0); i < numValues; i++ {
|
|
flag, bytesRead := deserializeVLQ(data[offset:])
|
|
offset += bytesRead
|
|
if bytesRead == 0 {
|
|
return nil, errDeserialize(fmt.Sprintf("unexpected end of "+
|
|
"data while reading value flag #%d", i))
|
|
}
|
|
|
|
value, bytesRead := deserializeVLQ(data[offset:])
|
|
offset += bytesRead
|
|
if bytesRead == 0 {
|
|
return nil, errDeserialize(fmt.Sprintf("unexpected end of "+
|
|
"data while reading value amount #%d", i))
|
|
}
|
|
|
|
typ := treasuryValueType(byte(flag) & tvFlagTypMask)
|
|
amount := int64(value)
|
|
|
|
// Debits (tspends and fees) are negative but stored as
|
|
// positive, so negate the amount if needed.
|
|
if typ.IsDebit() {
|
|
amount = -amount
|
|
}
|
|
values[i] = treasuryValue{
|
|
typ: typ,
|
|
amount: amount,
|
|
}
|
|
}
|
|
}
|
|
|
|
var ts treasuryState
|
|
ts.balance = int64(balance)
|
|
ts.values = values
|
|
return &ts, nil
|
|
}
|
|
|
|
// dbPutTreasuryBalance inserts a treasury state record into the database.
|
|
func dbPutTreasuryBalance(dbTx database.Tx, hash chainhash.Hash, ts treasuryState) error {
|
|
// Serialize the current treasury state.
|
|
serializedData, err := serializeTreasuryState(ts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store the current treasury state into the database.
|
|
meta := dbTx.Metadata()
|
|
bucket := meta.Bucket(treasuryBucketName)
|
|
return bucket.Put(hash[:], serializedData)
|
|
}
|
|
|
|
// dbFetchTreasuryBalance uses an existing database transaction to fetch the
|
|
// treasury state.
|
|
func dbFetchTreasuryBalance(dbTx database.Tx, hash chainhash.Hash) (*treasuryState, error) {
|
|
meta := dbTx.Metadata()
|
|
bucket := meta.Bucket(treasuryBucketName)
|
|
|
|
v := bucket.Get(hash[:])
|
|
if v == nil {
|
|
str := fmt.Sprintf("treasury db missing key: %v", hash)
|
|
return nil, errDbTreasury(str)
|
|
}
|
|
|
|
return deserializeTreasuryState(v)
|
|
}
|
|
|
|
// dbFetchTreasurySingle wraps dbFetchTreasuryBalance in a view.
|
|
func (b *BlockChain) dbFetchTreasurySingle(hash chainhash.Hash) (*treasuryState, error) {
|
|
var ts *treasuryState
|
|
err := b.db.View(func(dbTx database.Tx) error {
|
|
var err error
|
|
ts, err = dbFetchTreasuryBalance(dbTx, hash)
|
|
return err
|
|
})
|
|
return ts, err
|
|
}
|
|
|
|
// serializeTSpend serializes the TSpend data for use in the database.
|
|
// The format is as follows:
|
|
// Block []chainhash.Hash (blocks where TSpend was mined).
|
|
func serializeTSpend(blocks []chainhash.Hash) ([]byte, error) {
|
|
serializedData := new(bytes.Buffer)
|
|
err := binary.Write(serializedData, byteOrder, int64(len(blocks)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, v := range blocks {
|
|
err := binary.Write(serializedData, byteOrder, v[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return serializedData.Bytes(), nil
|
|
}
|
|
|
|
// deserializeTSpend deserializes a binary blob into a []chainhash.Hash.
|
|
func deserializeTSpend(data []byte) ([]chainhash.Hash, error) {
|
|
buf := bytes.NewReader(data)
|
|
var count int64
|
|
err := binary.Read(buf, byteOrder, &count)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read count: %w", err)
|
|
}
|
|
hashes := make([]chainhash.Hash, count)
|
|
for i := int64(0); i < count; i++ {
|
|
err := binary.Read(buf, byteOrder, &hashes[i])
|
|
if err != nil {
|
|
return nil,
|
|
fmt.Errorf("failed to read idx %v: %w", i, err)
|
|
}
|
|
}
|
|
|
|
return hashes, nil
|
|
}
|
|
|
|
// dbPutTSpend inserts a number of treasury tspends into a single database
|
|
// record. Note that this call is the low level write to the database. Use
|
|
// dbUpdateTSpend instead.
|
|
func dbPutTSpend(dbTx database.Tx, tx chainhash.Hash, blocks []chainhash.Hash) error {
|
|
// Serialize the current treasury state.
|
|
serializedData, err := serializeTSpend(blocks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store the current treasury state into the database.
|
|
meta := dbTx.Metadata()
|
|
bucket := meta.Bucket(treasuryTSpendBucketName)
|
|
return bucket.Put(tx[:], serializedData)
|
|
}
|
|
|
|
// errDbTSpend signifies that the provided hash was not found during a fetch.
|
|
type errDbTSpend string
|
|
|
|
// Error implements the error interface.
|
|
func (e errDbTSpend) Error() string {
|
|
return string(e)
|
|
}
|
|
|
|
// dbFetchTSpend uses an existing database transaction to fetch the block
|
|
// hashes that contains the provided transaction.
|
|
func dbFetchTSpend(dbTx database.Tx, tx chainhash.Hash) ([]chainhash.Hash, error) {
|
|
meta := dbTx.Metadata()
|
|
bucket := meta.Bucket(treasuryTSpendBucketName)
|
|
|
|
v := bucket.Get(tx[:])
|
|
if v == nil {
|
|
return nil, errDbTSpend(fmt.Sprintf("tspend db missing key: %v",
|
|
tx))
|
|
}
|
|
|
|
return deserializeTSpend(v)
|
|
}
|
|
|
|
// dbUpdateTSpend performs a read/modify/write operation on the provided
|
|
// transaction hash. It reads the record and appends the block hash and then
|
|
// writes it back to the database. Note that the append is dumb and does not
|
|
// deduplicate. This is ok because in practice a TX cannot appear in the same
|
|
// block more than once.
|
|
func dbUpdateTSpend(dbTx database.Tx, tx, block chainhash.Hash) error {
|
|
var derr errDbTSpend
|
|
hashes, err := dbFetchTSpend(dbTx, tx)
|
|
if err != nil && !errors.As(err, &derr) {
|
|
return err
|
|
}
|
|
|
|
hashes = append(hashes, block)
|
|
return dbPutTSpend(dbTx, tx, hashes)
|
|
}
|
|
|
|
// FetchTSpend returns the blocks a treasury spend tx was included in.
|
|
func (b *BlockChain) FetchTSpend(tspend chainhash.Hash) ([]chainhash.Hash, error) {
|
|
var hashes []chainhash.Hash
|
|
err := b.db.View(func(dbTx database.Tx) error {
|
|
var err error
|
|
hashes, err = dbFetchTSpend(dbTx, tspend)
|
|
return err
|
|
})
|
|
return hashes, err
|
|
}
|
|
|
|
// calculateTreasuryBalance calculates the treasury balance as of the provided
|
|
// node.
|
|
//
|
|
// The treasury balance for a given block is calculated as the balance of its
|
|
// parent block plus all maturing TADDs and TreasuryBases minus all maturing
|
|
// TSPENDS.
|
|
//
|
|
// The "maturing" TADDs, TreasuryBases and TSPENDS are those that were in the
|
|
// CoinbaseMaturity ancestor block of the passed node.
|
|
func (b *BlockChain) calculateTreasuryBalance(dbTx database.Tx, node *blockNode) int64 {
|
|
wantNode := node.RelativeAncestor(int64(b.chainParams.CoinbaseMaturity))
|
|
if wantNode == nil {
|
|
// Since the node does not exist we can safely assume the
|
|
// balance is 0. This is true at the beginning of the chain
|
|
// because before CoinbaseMaturity blocks there can be no
|
|
// mature treasurybase or funds from which to create a TADD.
|
|
return 0
|
|
}
|
|
|
|
// Current balance is in the parent node
|
|
ts, err := dbFetchTreasuryBalance(dbTx, node.parent.hash)
|
|
if err != nil {
|
|
// Since the node.parent.hash does not exist in the treasury db
|
|
// we can safely assume the balance is 0
|
|
return 0
|
|
}
|
|
|
|
// Fetch values that need to be added to the treasury balance.
|
|
wts, err := dbFetchTreasuryBalance(dbTx, wantNode.hash)
|
|
if err != nil {
|
|
// Since wantNode does not exist in the treasury db we can
|
|
// safely assume the balance is 0
|
|
return 0
|
|
}
|
|
|
|
// Add all TAdd values to the balance. Note that negative Values are
|
|
// TSpend.
|
|
var netValue int64
|
|
for _, v := range wts.values {
|
|
netValue += v.amount
|
|
}
|
|
|
|
return ts.balance + netValue
|
|
}
|
|
|
|
// dbPutTreasuryBalance inserts the current balance and the future treasury
|
|
// add/spend into the database.
|
|
func (b *BlockChain) dbPutTreasuryBalance(dbTx database.Tx, block *dcrutil.Block, node *blockNode) error {
|
|
// Calculate balance as of this node
|
|
balance := b.calculateTreasuryBalance(dbTx, node)
|
|
msgBlock := block.MsgBlock()
|
|
ts := treasuryState{
|
|
balance: balance,
|
|
values: make([]treasuryValue, 0, len(msgBlock.Transactions)*2),
|
|
}
|
|
trsyLog.Tracef("dbPutTreasuryBalance: %v start balance %v",
|
|
node.hash.String(), balance)
|
|
for _, v := range msgBlock.STransactions {
|
|
if stake.IsTAdd(v) {
|
|
// This is a TAdd, pull amount out of TxOut[0]. Note
|
|
// that TxOut[1], if it exists, contains the change
|
|
// output. We have to ignore change.
|
|
tv := treasuryValue{
|
|
typ: treasuryValueTAdd,
|
|
amount: v.TxOut[0].Value,
|
|
}
|
|
ts.values = append(ts.values, tv)
|
|
trsyLog.Tracef(" dbPutTreasuryBalance: balance TADD "+
|
|
"%v", tv.amount)
|
|
} else if stake.IsTreasuryBase(v) {
|
|
tv := treasuryValue{
|
|
typ: treasuryValueTBase,
|
|
amount: v.TxOut[0].Value,
|
|
}
|
|
ts.values = append(ts.values, tv)
|
|
trsyLog.Tracef(" dbPutTreasuryBalance: balance "+
|
|
"treasury base %v", tv.amount)
|
|
} else if stake.IsTSpend(v) {
|
|
// This is a TSpend, pull values out of block. Skip
|
|
// first TxOut since it is an OP_RETURN.
|
|
var totalOut int64
|
|
for _, vv := range v.TxOut[1:] {
|
|
tv := treasuryValue{
|
|
typ: treasuryValueTSpend,
|
|
amount: -vv.Value,
|
|
}
|
|
trsyLog.Tracef(" dbPutTreasuryBalance: "+
|
|
"balance TSPEND %v", tv.amount)
|
|
ts.values = append(ts.values, tv)
|
|
totalOut += vv.Value
|
|
}
|
|
|
|
// Fees are supposed to be stored as negative amounts
|
|
// in a treasuryValue value, so calculate it backwards
|
|
// from the usual way of `in - out`.
|
|
fee := totalOut - v.TxIn[0].ValueIn
|
|
tv := treasuryValue{
|
|
typ: treasuryValueFee,
|
|
amount: fee,
|
|
}
|
|
trsyLog.Tracef(" dbPutTreasuryBalance: "+
|
|
"balance fee %v", tv.amount)
|
|
ts.values = append(ts.values, tv)
|
|
}
|
|
}
|
|
|
|
hash := block.Hash()
|
|
return dbPutTreasuryBalance(dbTx, *hash, ts)
|
|
}
|
|
|
|
// dbPutTSpend inserts the TSpends that are included in this block to the
|
|
// database.
|
|
func (b *BlockChain) dbPutTSpend(dbTx database.Tx, block *dcrutil.Block) error {
|
|
hash := block.Hash()
|
|
msgBlock := block.MsgBlock()
|
|
trsyLog.Tracef("dbPutTSpend: processing block %v", hash)
|
|
for _, v := range msgBlock.STransactions {
|
|
if !stake.IsTSpend(v) {
|
|
continue
|
|
}
|
|
|
|
// Store TSpend and the block it was included in.
|
|
txHash := v.TxHash()
|
|
trsyLog.Tracef(" dbPutTSpend: tspend %v", txHash)
|
|
err := dbUpdateTSpend(dbTx, txHash, *hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TreasuryBalanceInfo models information about the treasury balance as of a
|
|
// given block.
|
|
type TreasuryBalanceInfo struct {
|
|
// BlockHeight is the height of the requested block.
|
|
BlockHeight int64
|
|
|
|
// Balance is the balance of the treasury as of the requested block.
|
|
Balance uint64
|
|
|
|
// Updates specifies all additions to and spends from the treasury in
|
|
// the requested block.
|
|
Updates []int64
|
|
}
|
|
|
|
// TreasuryBalance returns treasury balance information as of the given block.
|
|
func (b *BlockChain) TreasuryBalance(hash *chainhash.Hash) (*TreasuryBalanceInfo, error) {
|
|
node := b.index.LookupNode(hash)
|
|
if node == nil || !b.index.CanValidate(node) {
|
|
return nil, unknownBlockError(hash)
|
|
}
|
|
|
|
// Treasury agenda is never active for the genesis block.
|
|
if node.parent == nil {
|
|
str := fmt.Sprintf("treasury balance not available for block %s", hash)
|
|
return nil, contextError(ErrNoTreasuryBalance, str)
|
|
}
|
|
|
|
// Ensure the treasury agenda is active as of the requested block.
|
|
var isActive bool
|
|
b.chainLock.Lock()
|
|
isActive, err := b.isTreasuryAgendaActive(node.parent)
|
|
b.chainLock.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isActive {
|
|
str := fmt.Sprintf("treasury balance not available for block %s", hash)
|
|
return nil, contextError(ErrNoTreasuryBalance, str)
|
|
}
|
|
|
|
// Load treasury balance information.
|
|
var ts *treasuryState
|
|
err = b.db.View(func(dbTx database.Tx) error {
|
|
ts, err = dbFetchTreasuryBalance(dbTx, node.hash)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
updates := make([]int64, len(ts.values))
|
|
for i := range ts.values {
|
|
updates[i] = ts.values[i].amount
|
|
}
|
|
|
|
return &TreasuryBalanceInfo{
|
|
BlockHeight: node.height,
|
|
Balance: uint64(ts.balance),
|
|
Updates: updates,
|
|
}, nil
|
|
}
|
|
|
|
// verifyTSpendSignature verifies that the provided signature and public key
|
|
// were the ones that signed the provided message transaction.
|
|
func verifyTSpendSignature(msgTx *wire.MsgTx, signature, pubKey []byte) error {
|
|
// Calculate signature hash.
|
|
sigHash, err := txscript.CalcSignatureHash(nil,
|
|
txscript.SigHashAll, msgTx, 0, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("CalcSignatureHash: %w", err)
|
|
}
|
|
|
|
// Lift Signature from bytes.
|
|
sig, err := schnorr.ParseSignature(signature)
|
|
if err != nil {
|
|
return fmt.Errorf("ParseSignature: %w", err)
|
|
}
|
|
|
|
// Lift public PI key from bytes.
|
|
pk, err := schnorr.ParsePubKey(pubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("ParsePubKey: %w", err)
|
|
}
|
|
|
|
// Verify transaction was properly signed.
|
|
if !sig.Verify(sigHash, pk) {
|
|
return fmt.Errorf("Verify failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyTSpendSignature verifies that the provided signature and public key
|
|
// were the ones that signed the provided message transaction.
|
|
//
|
|
// Note: This function should only be called with a valid TSpend.
|
|
func VerifyTSpendSignature(msgTx *wire.MsgTx, signature, pubKey []byte) error {
|
|
return verifyTSpendSignature(msgTx, signature, pubKey)
|
|
}
|
|
|
|
// sumPastTreasuryChanges sums up the amounts spent from and added to the
|
|
// treasury (respectively) found within the range (node-nbBlocks..node). Note
|
|
// that this sum is _inclusive_ of the passed block and is performed in
|
|
// descending order. Generally, the passed node will be a node immediately
|
|
// before a TVI block.
|
|
//
|
|
// It also returns the node immediately before the last checked node (that is,
|
|
// the node before node-nbBlocks).
|
|
func (b *BlockChain) sumPastTreasuryChanges(preTVINode *blockNode, nbBlocks uint64) (int64, int64, *blockNode, error) {
|
|
node := preTVINode
|
|
var spent, added int64
|
|
var derr errDbTreasury
|
|
for i := uint64(0); i < nbBlocks && node != nil; i++ {
|
|
ts, err := b.dbFetchTreasurySingle(node.hash)
|
|
if errors.As(err, &derr) {
|
|
// Record doesn't exist. Means we reached the end of
|
|
// when treasury records are available.
|
|
node = nil
|
|
continue
|
|
} else if err != nil {
|
|
return 0, 0, nil, err
|
|
}
|
|
|
|
// Range over values.
|
|
for _, v := range ts.values {
|
|
if v.typ.IsDebit() {
|
|
// treasuryValues record debits as negative
|
|
// amounts, so invert it here.
|
|
spent += -v.amount
|
|
} else {
|
|
added += v.amount
|
|
}
|
|
}
|
|
|
|
node = node.parent
|
|
}
|
|
|
|
return spent, added, node, nil
|
|
}
|
|
|
|
// maxTreasuryExpenditureDCP0006 returns the maximum amount of funds that can
|
|
// be spent from the treasury at the block after the provided node.
|
|
//
|
|
// This is code that was activated as part of the 'treasury' consensus upgrade
|
|
// as defined in DCP0006.
|
|
//
|
|
// See notes on maxTreasuryExpenditure().
|
|
func (b *BlockChain) maxTreasuryExpenditureDCP0006(preTVINode *blockNode) (int64, error) {
|
|
// The expenditure policy check is roughly speaking:
|
|
//
|
|
// "The sum of tspends inside an expenditure window cannot exceed
|
|
// the average of the tspends in the previous N windows in addition
|
|
// to an X% increase."
|
|
//
|
|
// So in order to calculate the maximum expenditure for the _next_
|
|
// block (the block after `preTVINode`, which must be a TVI block) we
|
|
// need to add all the tspends in the expenditure window that ends in
|
|
// `preTVINode` and subtract that amount from the average of the
|
|
// preceding (N) windows.
|
|
//
|
|
// Currently this function is pretty naive. It simply iterates through
|
|
// prior blocks one at a time. This is very expensive and may need to
|
|
// be rethought into a proper index.
|
|
|
|
policyWindow := b.chainParams.TreasuryVoteInterval *
|
|
b.chainParams.TreasuryVoteIntervalMultiplier *
|
|
b.chainParams.TreasuryExpenditureWindow
|
|
|
|
// Each policy window starts at a TVI block and ends at the block
|
|
// immediately prior to another TVI (inclusive of preTVINode).
|
|
//
|
|
// First: sum up tspends inside the most recent policyWindow.
|
|
spentRecentWindow, _, node, err := b.sumPastTreasuryChanges(preTVINode, policyWindow)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Next, sum up all tspends inside the N prior policy windows. If a
|
|
// given policy window does not have _any_ tspends, it isn't counted
|
|
// towards the average.
|
|
var spentPriorWindows int64
|
|
var nbNonEmptyWindows int64
|
|
for i := uint64(0); i < b.chainParams.TreasuryExpenditurePolicy && node != nil; i++ {
|
|
var spent int64
|
|
spent, _, node, err = b.sumPastTreasuryChanges(node, policyWindow)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if spent > 0 {
|
|
spentPriorWindows += spent
|
|
nbNonEmptyWindows++
|
|
}
|
|
}
|
|
|
|
// Calculate the average spent in each window. If there were _zero_
|
|
// prior windows with tspends, fall back to using the bootstrap
|
|
// average.
|
|
var avgSpentPriorWindows int64
|
|
if nbNonEmptyWindows > 0 {
|
|
avgSpentPriorWindows = spentPriorWindows / nbNonEmptyWindows
|
|
} else {
|
|
avgSpentPriorWindows = int64(b.chainParams.TreasuryExpenditureBootstrap)
|
|
}
|
|
|
|
// Treasury can spend up to 150% the average amount of the prior
|
|
// windows ("expenditure allowance").
|
|
avgPlusAllowance := avgSpentPriorWindows + avgSpentPriorWindows/2
|
|
|
|
// The maximum expenditure allowed for the next block is the difference
|
|
// between the maximum possible and what has already been spent in the most
|
|
// recent policy window. This is capped at zero on the lower end to account
|
|
// for cases where the policy _already_ spent more than the allowed.
|
|
var allowedToSpend int64
|
|
if avgPlusAllowance > spentRecentWindow {
|
|
allowedToSpend = avgPlusAllowance - spentRecentWindow
|
|
}
|
|
|
|
trsyLog.Tracef(" maxTSpendExpenditure: recentWindow %d priorWindows %d "+
|
|
"(%d non-empty) allowedToSpend %d", spentRecentWindow,
|
|
spentPriorWindows, nbNonEmptyWindows, allowedToSpend)
|
|
|
|
return allowedToSpend, nil
|
|
}
|
|
|
|
// maxTreasuryExpenditureDCP0007 returns the maximum amount of funds that can
|
|
// be spent from the treasury at the block after the provided node.
|
|
//
|
|
// This is code that was activated as part of the 'reverttreasurypolicy'
|
|
// consensus upgrade as defined in DCP0007.
|
|
//
|
|
// See notes on maxTreasuryExpenditure().
|
|
func (b *BlockChain) maxTreasuryExpenditureDCP0007(preTVINode *blockNode) (int64, error) {
|
|
// The expenditure policy check is roughly speaking:
|
|
//
|
|
// "The sum of tspends inside an expenditure window cannot exceed
|
|
// the total amount of income received by the treasury in the same
|
|
// window in addition to an X% increase."
|
|
//
|
|
// Currently this function is pretty naive. It simply iterates through
|
|
// prior blocks one at a time. This is very expensive and may need to
|
|
// be rethought into a proper index.
|
|
|
|
policyWindow := b.chainParams.TreasuryVoteInterval *
|
|
b.chainParams.TreasuryVoteIntervalMultiplier *
|
|
b.chainParams.TreasuryExpenditureWindow
|
|
|
|
// Each policy window starts at a TVI block and ends at the block
|
|
// immediately prior to another TVI (inclusive of preTVINode).
|
|
//
|
|
// First: sum up tspends, tadds and tbases inside the most recent
|
|
// policyWindow.
|
|
spentRecent, addedRecent, _, err := b.sumPastTreasuryChanges(preTVINode, policyWindow)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Treasury can spend up to 150% the amount received in the previous
|
|
// window.
|
|
addedPlusAllowance := addedRecent + addedRecent/2
|
|
|
|
// The maximum expenditure allowed for the next block is the difference
|
|
// between the maximum possible and what has already been spent in the most
|
|
// recent policy window. This is capped at zero on the lower end to account
|
|
// for cases where the policy _already_ spent more than the allowed.
|
|
var allowedToSpend int64
|
|
if addedPlusAllowance > spentRecent {
|
|
allowedToSpend = addedPlusAllowance - spentRecent
|
|
}
|
|
|
|
trsyLog.Tracef(" maxTSpendExpenditureDCP0007: spent %d, "+
|
|
"added %d, allowedToSpend %d", spentRecent,
|
|
addedRecent, allowedToSpend)
|
|
|
|
return allowedToSpend, nil
|
|
}
|
|
|
|
// maxTreasuryExpenditure returns the maximum amount of funds that can be spent
|
|
// from the treasury at the block after the provided node. A set of TSPENDs
|
|
// added to a TVI block that is a child to the passed node may spend up to the
|
|
// returned amount.
|
|
//
|
|
// The passed node MUST correspond to a node immediately prior to a TVI block.
|
|
func (b *BlockChain) maxTreasuryExpenditure(preTVINode *blockNode) (int64, error) {
|
|
isRevertPolicyActive, err := b.isRevertTreasuryPolicyActive(preTVINode)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if isRevertPolicyActive {
|
|
return b.maxTreasuryExpenditureDCP0007(preTVINode)
|
|
}
|
|
return b.maxTreasuryExpenditureDCP0006(preTVINode)
|
|
}
|
|
|
|
// MaxTreasuryExpenditure is the maximum amount of funds that can be spent from
|
|
// the treasury by a set of TSpends for a block that extends the given block
|
|
// hash. Function will return 0 if it is called on an invalid TVI.
|
|
func (b *BlockChain) MaxTreasuryExpenditure(preTVIBlock *chainhash.Hash) (int64, error) {
|
|
preTVINode := b.index.LookupNode(preTVIBlock)
|
|
if preTVINode == nil {
|
|
return 0, fmt.Errorf("unknown block %s", preTVIBlock)
|
|
}
|
|
|
|
if !standalone.IsTreasuryVoteInterval(uint64(preTVINode.height+1),
|
|
b.chainParams.TreasuryVoteInterval) {
|
|
return 0, nil
|
|
}
|
|
|
|
return b.maxTreasuryExpenditure(preTVINode)
|
|
}
|
|
|
|
// checkTSpendsExpenditure verifies that the sum of TSpend expenditures is
|
|
// within the allowable range for the chain ending in the given node. There is
|
|
// a hard requirement that the passed node is a block immediately prior to a
|
|
// TVI block (with the tspends assumed to be txs in a block that extends
|
|
// preTVINode).
|
|
//
|
|
// The expenditure check is performed against the balance at preTVINode.
|
|
//
|
|
// This function must be called with the block index read lock held.
|
|
func (b *BlockChain) checkTSpendsExpenditure(preTVINode *blockNode, totalTSpendAmount int64) error {
|
|
trsyLog.Tracef("checkTSpendExpenditure: processing %d tspent at height %d",
|
|
totalTSpendAmount, preTVINode.height+1)
|
|
if totalTSpendAmount == 0 {
|
|
// Nothing to do.
|
|
return nil
|
|
}
|
|
if totalTSpendAmount < 0 {
|
|
return fmt.Errorf("invalid precondition: totalTSpendAmount must "+
|
|
"not be negative (got %d)", totalTSpendAmount)
|
|
}
|
|
|
|
// Ensure that we are not depleting treasury.
|
|
var (
|
|
treasuryBalance int64
|
|
err error
|
|
)
|
|
err = b.db.View(func(dbTx database.Tx) error {
|
|
treasuryBalance = b.calculateTreasuryBalance(dbTx, preTVINode)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if treasuryBalance-totalTSpendAmount < 0 {
|
|
return fmt.Errorf("treasury balance may not become negative: "+
|
|
"balance %v spend %v", treasuryBalance, totalTSpendAmount)
|
|
}
|
|
trsyLog.Tracef(" checkTSpendExpenditure: balance %v spend %v",
|
|
treasuryBalance, totalTSpendAmount)
|
|
|
|
allowedToSpend, err := b.maxTreasuryExpenditure(preTVINode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if totalTSpendAmount > allowedToSpend {
|
|
return fmt.Errorf("treasury spend greater than allowed %v > %v",
|
|
totalTSpendAmount, allowedToSpend)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkTSpendExists verifies that the provided TSpend has not been mined in a
|
|
// block on the chain of prevNode.
|
|
func (b *BlockChain) checkTSpendExists(prevNode *blockNode, tspend chainhash.Hash) error {
|
|
trsyLog.Tracef(" checkTSpendExists: tspend %v", tspend)
|
|
|
|
var derr errDbTSpend
|
|
blocks, err := b.FetchTSpend(tspend)
|
|
if errors.As(err, &derr) {
|
|
// Record does not exist.
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Do fork detection on all blocks.
|
|
for _, v := range blocks {
|
|
// Lookup blockNode.
|
|
node := b.index.LookupNode(&v)
|
|
if node == nil {
|
|
// This should not happen.
|
|
trsyLog.Errorf(" checkTSpendExists: block not found "+
|
|
"%v tspend %v", v, tspend)
|
|
continue
|
|
}
|
|
|
|
if !node.IsAncestorOf(prevNode) {
|
|
trsyLog.Errorf(" checkTSpendExists: not ancestor "+
|
|
"block %v tspend %v", v, tspend)
|
|
continue
|
|
}
|
|
trsyLog.Errorf(" checkTSpendExists: is ancestor "+
|
|
"block %v tspend %v", v, tspend)
|
|
return fmt.Errorf("tspend has already been mined on this "+
|
|
"chain %v", tspend)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckTSpendExists verifies that the provided TSpend has not been mined in a
|
|
// block on the chain of the provided block hash.
|
|
func (b *BlockChain) CheckTSpendExists(prevHash, tspend chainhash.Hash) error {
|
|
prevNode := b.index.LookupNode(&prevHash)
|
|
if prevNode == nil {
|
|
return nil
|
|
}
|
|
return b.checkTSpendExists(prevNode, tspend)
|
|
}
|
|
|
|
// getVotes returns yes and no votes for the provided hash.
|
|
func getVotes(votes []stake.TreasuryVoteTuple, hash *chainhash.Hash) (yes int, no int) {
|
|
if votes == nil {
|
|
return
|
|
}
|
|
|
|
for _, v := range votes {
|
|
if !hash.IsEqual(&v.Hash) {
|
|
continue
|
|
}
|
|
|
|
switch v.Vote {
|
|
case stake.TreasuryVoteYes:
|
|
yes++
|
|
case stake.TreasuryVoteNo:
|
|
no++
|
|
default:
|
|
// Can't happen.
|
|
trsyLog.Criticalf("getVotes: invalid vote 0x%v", v.Vote)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// tspendVotes is a structure that contains a treasury vote tally for a given
|
|
// window.
|
|
type tspendVotes struct {
|
|
start uint32 // Start block
|
|
end uint32 // End block
|
|
yes int // Yes vote tally
|
|
no int // No vote tally
|
|
}
|
|
|
|
// tSpendCountVotes returns the vote tally for a given tspend up to the
|
|
// specified block. Note that this function errors if the block is outside the
|
|
// voting window for the given tspend.
|
|
func (b *BlockChain) tSpendCountVotes(prevNode *blockNode, tspend *dcrutil.Tx) (*tspendVotes, error) {
|
|
trsyLog.Tracef("tSpendCountVotes: processing tspend %v", tspend.Hash())
|
|
|
|
var (
|
|
t tspendVotes
|
|
err error
|
|
)
|
|
|
|
expiry := tspend.MsgTx().Expiry
|
|
t.start, t.end, err = standalone.CalcTSpendWindow(expiry,
|
|
b.chainParams.TreasuryVoteInterval,
|
|
b.chainParams.TreasuryVoteIntervalMultiplier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nextHeight := prevNode.height + 1
|
|
trsyLog.Tracef(" tSpendCountVotes: nextHeight %v start %v expiry %v",
|
|
nextHeight, t.start, t.end)
|
|
|
|
// Ensure tspend is within the window.
|
|
if !standalone.InsideTSpendWindow(nextHeight,
|
|
expiry, b.chainParams.TreasuryVoteInterval,
|
|
b.chainParams.TreasuryVoteIntervalMultiplier) {
|
|
err = fmt.Errorf("tspend outside of window: nextHeight %v "+
|
|
"start %v expiry %v", nextHeight, t.start, expiry)
|
|
return nil, err
|
|
}
|
|
|
|
// Walk prevNode back to the start of the window and count votes.
|
|
node := prevNode
|
|
for {
|
|
trsyLog.Tracef(" tSpendCountVotes height %v start %v",
|
|
node.height, t.start)
|
|
if node.height < int64(t.start) {
|
|
break
|
|
}
|
|
|
|
trsyLog.Tracef(" tSpendCountVotes count votes: %v",
|
|
node.hash)
|
|
|
|
// Find SSGen and peel out votes.
|
|
var xblock *dcrutil.Block
|
|
xblock, err = b.fetchBlockByNode(node)
|
|
if err != nil {
|
|
// Should not happen.
|
|
return nil, err
|
|
}
|
|
for _, v := range xblock.STransactions() {
|
|
votes, err := stake.CheckSSGenVotes(v.MsgTx(),
|
|
yesTreasury)
|
|
if err != nil {
|
|
// Not an SSGEN
|
|
continue
|
|
}
|
|
|
|
// Find our vote bits.
|
|
yes, no := getVotes(votes, tspend.Hash())
|
|
t.yes += yes
|
|
t.no += no
|
|
}
|
|
|
|
node = node.parent
|
|
if node == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
// TSpendCountVotes tallies the votes given for the specified tspend during its
|
|
// voting interval, up to the specified block. It returns the number of yes and
|
|
// no votes found between the passed block's height and the start of voting.
|
|
//
|
|
// Note that this function errors if the block _after_ the specified block is
|
|
// outside the tpsend voting window. In particular, calling this function for
|
|
// the TVI block that ends the voting interval for a given tspend fails, since
|
|
// the next block is outside the voting interval.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (b *BlockChain) TSpendCountVotes(blockHash *chainhash.Hash, tspend *dcrutil.Tx) (yesVotes, noVotes int64, err error) {
|
|
b.index.RLock()
|
|
defer b.index.RUnlock()
|
|
|
|
prevNode := b.index.lookupNode(blockHash)
|
|
if prevNode == nil {
|
|
return 0, 0, unknownBlockError(blockHash)
|
|
}
|
|
tv, err := b.tSpendCountVotes(prevNode, tspend)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
return int64(tv.yes), int64(tv.no), nil
|
|
}
|
|
|
|
// checkTSpendHasVotes verifies that the provided TSpend has enough votes to be
|
|
// included in a block _after_ the provided block node. Such child node MUST be
|
|
// on a TVI.
|
|
func (b *BlockChain) checkTSpendHasVotes(prevNode *blockNode, tspend *dcrutil.Tx) error {
|
|
t, err := b.tSpendCountVotes(prevNode, tspend)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Passing criteria are 20% quorum and 60% yes.
|
|
maxVotes := uint32(b.chainParams.TicketsPerBlock) * (t.end - t.start)
|
|
quorum := uint64(maxVotes) * b.chainParams.TreasuryVoteQuorumMultiplier /
|
|
b.chainParams.TreasuryVoteQuorumDivisor
|
|
numVotesCast := uint64(t.yes + t.no)
|
|
if numVotesCast < quorum {
|
|
return fmt.Errorf("quorum not met: yes %v no %v "+
|
|
"quorum %v max %v", t.yes, t.no, quorum, maxVotes)
|
|
}
|
|
|
|
// Calculate max possible votes that are left in this window.
|
|
curBlockHeight := uint32(prevNode.height + 1)
|
|
remainingBlocks := t.end - curBlockHeight
|
|
maxRemainingVotes := uint64(remainingBlocks *
|
|
uint32(b.chainParams.TicketsPerBlock))
|
|
|
|
// Treat maxRemainingVotes as possible no votes. This enables short
|
|
// circuiting the overall TSpend vote but can only pass if the yes'
|
|
// can't drop below the required threshold.
|
|
requiredVotes := (numVotesCast + maxRemainingVotes) *
|
|
b.chainParams.TreasuryVoteRequiredMultiplier /
|
|
b.chainParams.TreasuryVoteRequiredDivisor
|
|
if uint64(t.yes) < requiredVotes {
|
|
return fmt.Errorf("not enough yes votes: yes %v no %v "+
|
|
"quorum %v max %v required %v maxRemainingVotes %v",
|
|
t.yes, t.no, quorum, maxVotes, requiredVotes,
|
|
maxRemainingVotes)
|
|
}
|
|
|
|
trsyLog.Infof("TSpend %v passed with: yes %v no %v quorum %v "+
|
|
"required %v", tspend.Hash(), t.yes, t.no, quorum,
|
|
requiredVotes)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckTSpendHasVotes checks whether the given tspend has enough votes to be
|
|
// included in a block _after_ the specified prevHash block.
|
|
//
|
|
// Such child block MUST be on a TVI, otherwise the result of this function may
|
|
// not correspond to the behavior of the consensus rules.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (b *BlockChain) CheckTSpendHasVotes(prevHash chainhash.Hash, tspend *dcrutil.Tx) error {
|
|
prevNode := b.index.LookupNode(&prevHash)
|
|
if prevNode == nil {
|
|
return unknownBlockError(&prevHash)
|
|
}
|
|
return b.checkTSpendHasVotes(prevNode, tspend)
|
|
}
|