dcrd/blockchain/treasury_test.go
Dave Collins 175d680afc
blockchain: Convert to use stdscript.
This converts the blockchain package to use the stdscript package
instead of txscript.

This is part of a series of commits to convert all packages in the
repository to make use of the new stdscript package.
2021-11-18 12:53:27 -06:00

4326 lines
138 KiB
Go

// Copyright (c) 2020-2021 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"
"os"
"reflect"
"testing"
"time"
"github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/blockchain/standalone/v2"
"github.com/decred/dcrd/blockchain/v4/chaingen"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/database/v3"
"github.com/decred/dcrd/dcrec"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/gcs/v3/blockcf2"
"github.com/decred/dcrd/lru"
"github.com/decred/dcrd/txscript/v4"
"github.com/decred/dcrd/txscript/v4/sign"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/txscript/v4/stdscript"
"github.com/decred/dcrd/wire"
)
var (
// These keys come from chaincfg/regnetparams.go
privKey = hexToBytes("68ab7efdac0eb99b1edf83b23374cc7a9c8d0a4183a2627afc8ea0437b20589e")
privKey2 = hexToBytes("2527f13f61024c9b9f4b30186f16e0b0af35b08c54ed2ed67def863b447ea11b")
)
// TestTreasuryValueTypeDebits ensures the IsDebit() method of
// treasuryValueType works as expected.
func TestTreasuryValueTypeDebits(t *testing.T) {
tests := []struct {
name string
typ treasuryValueType
wantDebit bool
}{{
name: "treasuryValueTBase",
typ: treasuryValueTBase,
wantDebit: false,
}, {
name: "treasuryValueTAdd",
typ: treasuryValueTAdd,
wantDebit: false,
}, {
name: "treasuryValueFee",
typ: treasuryValueFee,
wantDebit: true,
}, {
name: "treasuryValueTSpend",
typ: treasuryValueTSpend,
wantDebit: true,
}}
for _, test := range tests {
gotDebit := test.typ.IsDebit()
if gotDebit != test.wantDebit {
t.Fatalf("mismatched IsDebit result for %s -- got %v, "+
"want %v", test.name, gotDebit, test.wantDebit)
}
}
}
// TestTreasuryStateSerialization ensures serializing and deserializing treasury
// states works as expected, including the possible error conditions.
func TestTreasuryStateSerialization(t *testing.T) {
tests := []struct {
name string
state *treasuryState
serialized []byte
encodeErr error
decodeErr error
}{{
name: "equal",
state: &treasuryState{
balance: 100,
values: []treasuryValue{
{treasuryValueTBase, 1},
{treasuryValueTAdd, 2},
{treasuryValueTAdd, 3},
{treasuryValueTSpend, -3},
{treasuryValueFee, -2},
},
},
serialized: hexToBytes("640501010202020304030302"),
}, {
name: "negative balance",
state: &treasuryState{
balance: -100,
values: []treasuryValue{
{treasuryValueTBase, 1},
{treasuryValueTAdd, 2},
{treasuryValueTAdd, 3},
{treasuryValueTSpend, -3},
{treasuryValueFee, -2},
},
},
encodeErr: errDbTreasury(""),
decodeErr: errDeserialize(""),
}, {
name: "empty data",
state: nil,
serialized: hexToBytes(""),
decodeErr: errDeserialize(""),
}, {
name: "no data after balance",
state: nil,
serialized: hexToBytes("64"),
decodeErr: errDeserialize(""),
}, {
name: "no data after num values",
state: nil,
serialized: hexToBytes("6401"),
decodeErr: errDeserialize(""),
}, {
name: "no data after first value",
state: nil,
serialized: hexToBytes("640201"),
decodeErr: errDeserialize(""),
}, {
name: "trying to serialize incorrect negative tbase",
state: &treasuryState{
values: []treasuryValue{{typ: treasuryValueTBase, amount: -1}},
},
encodeErr: errDbTreasury(""),
serialized: nil,
decodeErr: errDeserialize(""),
}, {
name: "trying to serialize incorrect negative tadd",
state: &treasuryState{
values: []treasuryValue{{typ: treasuryValueTAdd, amount: -1}},
},
encodeErr: errDbTreasury(""),
serialized: nil,
decodeErr: errDeserialize(""),
}, {
name: "trying to serialize incorrect positive tspend",
state: &treasuryState{
values: []treasuryValue{{typ: treasuryValueTSpend, amount: 1}},
},
encodeErr: errDbTreasury(""),
serialized: nil,
decodeErr: errDeserialize(""),
}, {
name: "trying to serialize incorrect positive fee",
state: &treasuryState{
values: []treasuryValue{{typ: treasuryValueFee, amount: 1}},
},
encodeErr: errDbTreasury(""),
serialized: nil,
decodeErr: errDeserialize(""),
}, {
name: "serialize zero amounts for all value types",
state: &treasuryState{
balance: 100,
values: []treasuryValue{
{typ: treasuryValueTBase, amount: 0},
{typ: treasuryValueTAdd, amount: 0},
{typ: treasuryValueTSpend, amount: 0},
{typ: treasuryValueFee, amount: 0},
},
},
serialized: hexToBytes("64040100020004000300"),
}}
for _, test := range tests {
// Ensure the serialized bytes are deserialized back to the expected
// treasury state.
gotState, err := deserializeTreasuryState(test.serialized)
if !errors.Is(err, test.decodeErr) {
t.Errorf("%s: mismatched err -- got %v, want %v", test.name, err,
test.decodeErr)
continue
}
if test.decodeErr == nil && !reflect.DeepEqual(gotState, test.state) {
t.Errorf("%s: mismatched states\ngot %+v\nwant %+v", test.name,
gotState, test.state)
continue
}
// Skip serialization tests for nil states.
if test.state == nil {
continue
}
// Ensure the treasury state serializes to the expected value.
gotSerialized, err := serializeTreasuryState(*test.state)
if !errors.Is(err, test.encodeErr) {
t.Errorf("%s: mismatched err -- got %v, want %v", test.name, err,
test.encodeErr)
}
if !bytes.Equal(gotSerialized, test.serialized) {
t.Errorf("%s: did not get expected bytes - got %x, want %x",
test.name, gotSerialized, test.serialized)
continue
}
}
}
// TestTreasuryDatabase tests treasury database functionality.
func TestTreasuryDatabase(t *testing.T) {
// Create a new database to store treasury state.
dbName := "ffldb_treasurydb_test"
dbPath, err := os.MkdirTemp("", dbName)
if err != nil {
t.Fatalf("unable to create treasury db path: %v", err)
}
defer os.RemoveAll(dbPath)
net := chaincfg.RegNetParams().Net
testDb, err := database.Create(testDbType, dbPath, net)
if err != nil {
t.Fatalf("error creating treasury db: %v", err)
}
defer testDb.Close()
// Add bucket.
err = testDb.Update(func(dbTx database.Tx) error {
_, err := dbTx.Metadata().CreateBucketIfNotExists(treasuryBucketName)
return err
})
if err != nil {
t.Fatal(err)
}
// Write maxTreasuryState records out.
maxTreasuryState := uint64(1024)
for i := uint64(0); i < maxTreasuryState; i++ {
// Create synthetic treasury state
ts := treasuryState{
balance: int64(i),
values: []treasuryValue{
{typ: treasuryValueTBase, amount: int64(i)},
{typ: treasuryValueTSpend, amount: -1 - int64(i)},
},
}
// Create hash of counter.
b := make([]byte, 16)
binary.LittleEndian.PutUint64(b[0:], i)
hash := chainhash.HashH(b)
err = testDb.Update(func(dbTx database.Tx) error {
return dbPutTreasuryBalance(dbTx, hash, ts)
})
if err != nil {
t.Fatalf("%v", err)
}
}
// Pull records back out.
for i := uint64(0); i < maxTreasuryState; i++ {
// Create synthetic treasury state
ts := treasuryState{
balance: int64(i),
values: []treasuryValue{
{typ: treasuryValueTBase, amount: int64(i)},
{typ: treasuryValueTSpend, amount: -1 - int64(i)},
},
}
// Create hash of counter.
b := make([]byte, 16)
binary.LittleEndian.PutUint64(b[0:], i)
hash := chainhash.HashH(b)
var tsr *treasuryState
err = testDb.View(func(dbTx database.Tx) error {
tsr, err = dbFetchTreasuryBalance(dbTx, hash)
return err
})
if err != nil {
t.Fatalf("%v", err)
}
if !reflect.DeepEqual(ts, *tsr) {
t.Fatalf("not same treasury state got %v wanted %v", ts, *tsr)
}
}
}
// TestTspendDatabase tests tspend database functionality including
// serialization and deserialization.
func TestTSpendDatabase(t *testing.T) {
// Create a new database to store treasury state.
dbName := "ffldb_tspenddb_test"
dbPath, err := os.MkdirTemp("", dbName)
if err != nil {
t.Fatalf("unable to create tspend db path: %v", err)
}
defer os.RemoveAll(dbPath)
net := chaincfg.RegNetParams().Net
testDb, err := database.Create(testDbType, dbPath, net)
if err != nil {
t.Fatalf("error creating tspend db: %v", err)
}
defer testDb.Close()
// Add bucket.
err = testDb.Update(func(dbTx database.Tx) error {
_, err := dbTx.Metadata().CreateBucketIfNotExists(treasuryTSpendBucketName)
return err
})
if err != nil {
t.Fatal(err)
}
// Write maxTSpendState records out.
maxTSpendState := uint64(8)
txHash := chainhash.Hash{}
for i := uint64(0); i < maxTSpendState; i++ {
// Create hash of counter.
b := make([]byte, 16)
binary.LittleEndian.PutUint64(b[0:], i)
blockHash := chainhash.HashH(b)
err = testDb.Update(func(dbTx database.Tx) error {
return dbUpdateTSpend(dbTx, txHash, blockHash)
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// Pull records back out.
var hashes []chainhash.Hash
err = testDb.View(func(dbTx database.Tx) error {
hashes, err = dbFetchTSpend(dbTx, txHash)
return err
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for i := uint64(0); i < maxTSpendState; i++ {
// Create hash of counter.
b := make([]byte, 16)
binary.LittleEndian.PutUint64(b[0:], i)
hash := chainhash.HashH(b)
if !hash.IsEqual(&hashes[i]) {
t.Fatalf("not same tspend hash got %v wanted %v", hashes[i], hash)
}
}
}
// appendHashes takes a slice of chainhash and votebits and appends it all
// together for a TV script.
func appendHashes(tspendHashes []*chainhash.Hash, votes []stake.TreasuryVoteT) []byte {
if len(tspendHashes) != len(votes) {
panic(fmt.Sprintf("assert appendHashes %v != %v", len(tspendHashes),
len(votes)))
}
blob := make([]byte, 0, 2+chainhash.HashSize*7+7)
blob = append(blob, 'T', 'V')
for k, v := range tspendHashes {
blob = append(blob, v[:]...)
blob = append(blob, byte(votes[k]))
}
return blob
}
// addTSpendVotes return a munge function that votes according to voteBits.
func addTSpendVotes(t *testing.T, tspendHashes []*chainhash.Hash, votes []stake.TreasuryVoteT, nrVotes uint16, skipAssert bool) func(*wire.MsgBlock) {
if len(tspendHashes) != len(votes) {
panic(fmt.Sprintf("assert addTSpendVotes %v != %v", len(tspendHashes),
len(votes)))
}
return func(b *wire.MsgBlock) {
// Find SSGEN and append votes.
for k, v := range b.STransactions {
if !stake.IsSSGen(v, yesTreasury) {
continue
}
if len(v.TxOut) != 3 {
t.Fatalf("expected SSGEN.TxOut len 3 got %v", len(v.TxOut))
}
// Only allow provided number of votes.
if uint16(k) > nrVotes {
break
}
// Append vote:
// OP_RETURN OP_DATA <TV> <tspend hash> <vote bits>
vote := appendHashes(tspendHashes, votes)
s, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).
AddData(vote).Script()
if err != nil {
t.Fatal(err)
}
b.STransactions[k].TxOut = append(b.STransactions[k].TxOut,
&wire.TxOut{
PkScript: s,
})
// Only TxVersionTreasury supports optional votes.
b.STransactions[k].Version = wire.TxVersionTreasury
// See if we should skip asserts. This is used for
// munging votes and bits.
if skipAssert {
continue
}
// Assert vote insertion worked.
_, err = stake.GetSSGenTreasuryVotes(s)
if err != nil {
t.Fatalf("expected treasury vote: %v", err)
}
// Assert this remains a valid SSGEN.
err = stake.CheckSSGen(b.STransactions[k], yesTreasury)
if err != nil {
t.Fatalf("expected SSGen: %v", err)
}
}
}
}
const devsub = 5000000000
// standardTreasurybaseOpReturn creates a standard OP_RETURN output to insert
// into coinbase. This function autogenerates the extranonce. The OP_RETURN
// pushes 12 bytes.
func standardTreasurybaseOpReturn(height uint32) []byte {
extraNonce, err := wire.RandomUint64()
if err != nil {
panic(err)
}
enData := make([]byte, 12)
binary.LittleEndian.PutUint32(enData[0:4], height)
binary.LittleEndian.PutUint64(enData[4:12], extraNonce)
extraNonceScript, err := stdscript.ProvablyPruneableScriptV0(enData)
if err != nil {
panic(err)
}
return extraNonceScript
}
// replaceCoinbase is a munge function that takes the coinbase and removes the
// treasury payout and moves it to a TADD treasury agenda based version. It
// also bumps all STransactions indexes by 1 since we require treasurybase to
// be the 0th entry in the stake tree.
func replaceCoinbase(b *wire.MsgBlock) {
// Find coinbase tx and remove dev subsidy.
coinbaseTx := b.Transactions[0]
devSubsidy := coinbaseTx.TxOut[0].Value
coinbaseTx.TxOut = coinbaseTx.TxOut[1:]
coinbaseTx.Version = wire.TxVersionTreasury
coinbaseTx.TxIn[0].ValueIn -= devSubsidy
// Create treasuryBase and insert it at position 0 of the stake
// tree.
oldSTransactions := b.STransactions
b.STransactions = make([]*wire.MsgTx, len(b.STransactions)+1)
for k, v := range oldSTransactions {
b.STransactions[k+1] = v
}
treasurybaseTx := wire.NewMsgTx()
treasurybaseTx.Version = wire.TxVersionTreasury
treasurybaseTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
wire.MaxPrevOutIndex, wire.TxTreeRegular),
Sequence: wire.MaxTxInSequenceNum,
BlockHeight: wire.NullBlockHeight,
BlockIndex: wire.NullBlockIndex,
// Required 0 len SignatureScript
})
treasurybaseTx.TxIn[0].ValueIn = devSubsidy
treasurybaseTx.AddTxOut(&wire.TxOut{
Value: devSubsidy,
PkScript: []byte{txscript.OP_TADD},
})
// Encoded height.
treasurybaseTx.AddTxOut(&wire.TxOut{
Value: 0,
PkScript: standardTreasurybaseOpReturn(b.Header.Height),
})
retTx := dcrutil.NewTx(treasurybaseTx)
retTx.SetTree(wire.TxTreeStake)
b.STransactions[0] = retTx.MsgTx()
// Sanity check treasury base.
if !standalone.IsTreasuryBase(retTx.MsgTx()) {
panic(stake.CheckTreasuryBase(retTx.MsgTx()))
}
}
func TestTSpendVoteCount(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
startTip := g.TipName()
// ---------------------------------------------------------------------
// Create TSpend in "mempool"
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
tspendAmount := devsub
tspendFee := 100
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, end, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
}}, dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// ---------------------------------------------------------------------
// Try to insert TSPEND while not on a TVI
//
// ... -> bva19
// \-> bnottvi0
// ---------------------------------------------------------------------
// Assert we are not on a TVI and generate block. This should fail.
if standalone.IsTreasuryVoteInterval(uint64(nextBlockHeight), tvi) {
t.Fatalf("expected !TVI %v", nextBlockHeight)
}
outs := g.OldestCoinbaseOuts()
name := "bnottvi0"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrNotTVI)
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// \-> bnottvi0
// ---------------------------------------------------------------------
// Generate votes up to TVI. This is legal however they should NOT be
// counted in the totals since they are outside of the voting window.
g.SetTip(startTip)
voteCount := params.TicketsPerBlock
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Add TSpend on first block of window. This should fail with not
// enough votes.
//
// ... -> bpretvi1
// \-> btvinotenough0
// ---------------------------------------------------------------------
// Assert we are on a TVI and generate block. This should fail.
startTip = g.TipName()
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
name = "btvinotenough0"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrNotEnoughTSpendVotes)
// ---------------------------------------------------------------------
// Generate 1 TVI of No votes and add TSpend,
//
// ... -> bpretvi1 -> btvi0 -> ... -> btvi3
// ---------------------------------------------------------------------
g.SetTip(startTip)
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("btvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteNo},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert we are on a TVI and generate block. This should fail.
startTip = g.TipName()
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
name = "btvinotenough1"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrNotEnoughTSpendVotes)
// ---------------------------------------------------------------------
// Generate two more TVI of no votes.
//
// ... -> btvinotenough0 -> btvi4 -> ... -> btvi7
// ---------------------------------------------------------------------
g.SetTip(startTip)
for i := uint64(0); i < tvi*2; i++ {
name := fmt.Sprintf("btvi%v", tvi+i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteNo},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert we are on a TVI and generate block. This should fail with No
// vote (TSpend should not have been submitted).
startTip = g.TipName()
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
name = "btvienough0"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrNotEnoughTSpendVotes)
// Assert we have the correct number of votes and voting window.
tv, err := g.chain.tSpendCountVotes(g.chain.bestChain.Tip(),
dcrutil.NewTx(tspend))
if err != nil {
t.Fatal(err)
}
if start != tv.start {
t.Fatalf("invalid start block got %v wanted %v", tv.start, start)
}
if end != tv.end {
t.Fatalf("invalid end block got %v wanted %v", tv.end, end)
}
expectedYesVotes := 0 // We voted a bunch of times outside the window
expectedNoVotes := tvi * mul * uint64(params.TicketsPerBlock)
if expectedYesVotes != tv.yes {
t.Fatalf("invalid yes votes got %v wanted %v",
expectedYesVotes, tv.yes)
}
if expectedNoVotes != uint64(tv.no) {
t.Fatalf("invalid no votes got %v wanted %v",
expectedNoVotes, tv.no)
}
// ---------------------------------------------------------------------
// Generate one more TVI and append expired TSpend.
//
// ... -> bposttvi0 ... -> bposttvi3 ->
// \-> bexpired0
// ---------------------------------------------------------------------
g.SetTip(startTip)
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("bposttvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert TSpend expired
startTip = g.TipName()
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
name = "bexpired0"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrExpiredTx)
// ---------------------------------------------------------------------
// Create TSpend in "mempool"
//
// Test corner of quorum-1 vote and exact quorum yes vote.
// ---------------------------------------------------------------------
// Use exact hight to validate that tspend starts on next tvi.
expiry = standalone.CalcTSpendExpiry(int64(g.Tip().Header.Height), tvi, mul)
start, _, err = standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
// While here test that start is next tvi while on tvi.
if g.Tip().Header.Height+uint32(tvi) != start {
t.Fatalf("expected to see exactly next tvi got %v wanted %v",
start, g.Tip().Header.Height+uint32(tvi))
}
tspend = g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash = tspend.TxHash()
// Fast forward to next tvi and add no votes which should not count.
g.SetTip(startTip)
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("bnovote%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteNo},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Hit quorum-1 yes votes.
maxVotes := uint32(params.TicketsPerBlock) *
(tv.end - tv.start)
quorum := uint64(maxVotes) * params.TreasuryVoteQuorumMultiplier /
params.TreasuryVoteQuorumDivisor
totalVotes := uint16(quorum - 1)
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("byesvote%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
totalVotes, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
if totalVotes > params.TicketsPerBlock {
totalVotes -= params.TicketsPerBlock
} else {
totalVotes = 0
}
}
// Verify we are one vote shy of quorum
startTip = g.TipName()
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
name = "bquorum0"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrNotEnoughTSpendVotes)
// Count votes.
tv, err = g.chain.tSpendCountVotes(g.chain.bestChain.Tip(),
dcrutil.NewTx(tspend))
if err != nil {
t.Fatal(err)
}
if int(quorum-1) != tv.yes {
t.Fatalf("unexpected yesVote count got %v wanted %v",
tv.yes, quorum-1)
}
// Hit exact yes vote quorum
g.SetTip(startTip)
totalVotes = uint16(1)
for i := uint64(0); i < tvi*2; i++ {
name := fmt.Sprintf("byesvote%v", tvi+i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
totalVotes, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
if totalVotes > params.TicketsPerBlock {
totalVotes -= params.TicketsPerBlock
} else {
totalVotes = 0
}
}
// Count votes.
tv, err = g.chain.tSpendCountVotes(g.chain.bestChain.Tip(),
dcrutil.NewTx(tspend))
if err != nil {
t.Fatal(err)
}
if int(quorum) != tv.yes {
t.Fatalf("unexpected yesVote count got %v wanted %v",
tv.yes, quorum)
}
// Verify TSpend can be added exactly on quorum.
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
name = "bquorum1"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.AcceptTipBlock()
}
// getTreasuryState retrieves the treasury state for the provided hash.
func getTreasuryState(g *chaingenHarness, hash chainhash.Hash) (*treasuryState, error) {
var (
tsr *treasuryState
err error
)
err = g.chain.db.View(func(dbTx database.Tx) error {
tsr, err = dbFetchTreasuryBalance(dbTx, hash)
return err
})
return tsr, nil
}
// TestTSpendEmptyTreasury tests that we can't generate a tspend that spends
// more funds than available in the treasury even when otherwise allowed by
// policy.
func TestTSpendEmptyTreasury(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// To ensure we can completely drain the treasury, increase the
// bootstrap policy to allow a tspend with a _very_ high value.
params.TreasuryExpenditureBootstrap = 21e6 * 1e8
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create TSPEND in mempool for exact amount of treasury + 1 atom
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspendAmount := devsub*(tvi*mul-uint64(params.CoinbaseMaturity)+
uint64(start-nextBlockHeight)) + 1 // One atom too many
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
// Generate votes up to TVI. This is legal however they should NOT be
// counted in the totals since they are outside of the voting window.
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate a TVI worth of rewards and try to spend more.
//
// ... -> b0 ... -> b7
// \-> btoomuch0
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("b%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert treasury balance is 1 atom less than calculated amount.
ts, err := getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if int64(tspendAmount-tspendFee)-ts.balance != 1 {
t.Fatalf("Assert treasury balance error: got %v want %v",
ts.balance, int64(tspendAmount-tspendFee)-ts.balance)
}
// Try spending 1 atom more than treasury balance.
name := "btoomuch0"
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrInvalidExpenditure)
}
// TestExpendituresReorg tests that the correct treasury balance is tracked
// when reorgs remove tspends/tadds from the main chain. Test plan:
//
// - Approve and mine TSpend and TAdd
// - Reorg to chain without TSpend/Tadd
// - Mine until TSpend/TAdd would have been mature
// - Extend TSpend/TAdd chain and reorg back to it
func TestExpendituresReorg(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// Helper to check the treasury balance at tip.
assertTipTreasuryBalance := func(wantBalance int64) {
t.Helper()
ts, err := getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if ts.balance != wantBalance {
t.Fatalf("unexpected treasury balance. want=%d got %d",
wantBalance, ts.balance)
}
}
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// tbaseBlocks will keep track of how many blocks with a treasury base
// have been added to the chain.
var tbaseBlocks int
// ---------------------------------------------------------------------
// Generate enough blocks to have spendable outputs during the reorg
// stage of the test. Due to having lowered the coinbase maturity,
// we'll need to manually generate and store these outputs until we
// have enough to create two sidechains in the future.
//
// ... -> bva19 -> bout0 -> .. boutn
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
neededBlocks := (params.CoinbaseMaturity + 1) * 2
neededOuts := neededBlocks * params.TicketsPerBlock
oldOuts := make([][]chaingen.SpendableOut, 1)
for i := uint32(0); i < uint32(neededOuts); i++ {
name := fmt.Sprintf("bout%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
lastOuts := &oldOuts[len(oldOuts)-1]
*lastOuts = append(*lastOuts, outs[0])
if len(*lastOuts) == int(params.TicketsPerBlock) && i < uint32(neededOuts-1) {
oldOuts = append(oldOuts, make([]chaingen.SpendableOut, 0, int(params.TicketsPerBlock)))
}
tbaseBlocks++
}
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
// Generate up to TVI blocks.
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
tbaseBlocks++
}
// Generate a TSpend for some amount.
tspendAmount := int64(1e7)
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{{
Amount: dcrutil.Amount(uint64(tspendAmount) - tspendFee),
}}, dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// Generate a TAdd for some amount.
taddAmount := int64(1701)
taddFee := dcrutil.Amount(0)
tadd := g.CreateTreasuryTAdd(&outs[0], dcrutil.Amount(taddAmount), taddFee)
tadd.Version = wire.TxVersionTreasury
// ---------------------------------------------------------------------
// Generate enough blocks to approve the tspend.
//
// ... -> bpretvi1 -> bv0 -> ... -> bvn
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("bv%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
tbaseBlocks++
}
// Remember the block immediately before the TSpend is mined so we can
// create sidechains.
preTSpendBlock := g.TipName()
// ---------------------------------------------------------------------
// Generate a block that includes the tspend and tadd.
//
// ... -> bvn -> btspend
// ---------------------------------------------------------------------
g.NextBlock("btspend", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
b.AddSTransaction(tspend)
b.AddSTransaction(tadd)
})
g.AcceptTipBlock()
// Balance only reflects the tbase added so far. We add 1 to
// tbaseBlocks to account for the `btspend` block.
wantBalance := (int64(tbaseBlocks+1) - int64(params.CoinbaseMaturity)) * devsub
assertTipTreasuryBalance(wantBalance)
// ---------------------------------------------------------------------
// Generate a sidechain that does _not_ include the TSpend and mine it
// all the way to when the TSpend would have been mature and ensure the
// treasury balance does _not_ reflect the reorged txs.
//
// ... -> bvn -> btspend
// \-> bnotxs0 -> ... -> bnotxsn
// ---------------------------------------------------------------------
g.SetTip(preTSpendBlock)
blocksToTreasuryChange := uint64(params.CoinbaseMaturity + 1)
for i := uint64(0); i < blocksToTreasuryChange; i++ {
name := fmt.Sprintf("bnotxs%v", i)
g.NextBlock(name, nil, oldOuts[0], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
switch i {
case 0:
// The first block creates a sidechain.
g.AcceptedToSideChainWithExpectedTip("btspend")
default:
g.AcceptTipBlock()
}
oldOuts = oldOuts[1:]
tbaseBlocks++
}
// Given we reorged to a side chain that does _not_ include the TSpend
// and TAdd, we expect the current balance to have only tbases.
wantBalance = (int64(tbaseBlocks) - int64(params.CoinbaseMaturity)) * devsub
assertTipTreasuryBalance(wantBalance)
// ---------------------------------------------------------------------
// Extend the chain starting at btspend again, until it becomes the
// main chain again.
//
// ... -> bvn -> btspend -> btspend0 -> ... btspendn
// \-> bnotxs0 -> ... -> bnotxsn
// ---------------------------------------------------------------------
tip := g.TipName()
g.SetTip("btspend")
for i := uint64(0); i < blocksToTreasuryChange; i++ {
name := fmt.Sprintf("btspend%v", i)
g.NextBlock(name, nil, oldOuts[0], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
switch {
case i < blocksToTreasuryChange-1:
// Only the last block changes tip.
g.AcceptedToSideChainWithExpectedTip(tip)
default:
g.AcceptTipBlock()
}
oldOuts = oldOuts[1:]
}
// We mined one more block vs the previous chain.
tbaseBlocks++
// We reorged back to the chain that includes the mature TSpend/TAdd so
// now ensure the treasury balance reflects their presence.
wantBalance = (int64(tbaseBlocks)-int64(params.CoinbaseMaturity))*devsub -
tspendAmount + taddAmount
assertTipTreasuryBalance(wantBalance)
}
// TestSpendableTreasuryTxs tests that the outputs of mined TSpends and TAdds
// are actually spendable by transactions that can be mined.
//
// - Approve and mine TSpend and TAdd
// - Mine until TSpend/TAdd are mature
// - Create a tx that spends from the TSpend and TAdd change.
func TestSpendableTreasuryTxs(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
// Generate up to TVI blocks.
outs := g.OldestCoinbaseOuts()
taddOut1 := outs[0] // Save this output for a tadd.
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Create P2PKH and P2SH scripts to test spendability of TSpend and
// TAdd outputs. Each one is unique so we can do a cfilter test after
// mining these txs.
spendPrivKey := secp256k1.NewPrivateKey(new(secp256k1.ModNScalar).SetInt(1))
pubKey := spendPrivKey.PubKey().SerializeCompressed()
pubKeyHash := stdaddr.Hash160(pubKey)
spendP2pkhAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(
pubKeyHash, params)
if err != nil {
t.Fatal(err)
}
spendP2shScript := []byte{txscript.OP_NOP, txscript.OP_TRUE}
spendP2shAddr, err := stdaddr.NewAddressScriptHashV0(spendP2shScript,
params)
if err != nil {
t.Fatal(err)
}
addPrivKey := secp256k1.NewPrivateKey(new(secp256k1.ModNScalar).SetInt(2))
pubKey = addPrivKey.PubKey().SerializeCompressed()
pubKeyHash = stdaddr.Hash160(pubKey)
addP2pkhAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(
pubKeyHash, params)
if err != nil {
t.Fatal(err)
}
addP2shScript := []byte{txscript.OP_NOP, txscript.OP_NOP, txscript.OP_TRUE}
addP2shAddr, err := stdaddr.NewAddressScriptHashV0(addP2shScript, params)
if err != nil {
t.Fatal(err)
}
// Generate a TSpend for some amount.
tspendAmount := int64(1e7)
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(uint64(tspendAmount) - tspendFee),
Address: spendP2pkhAddr,
},
{
Amount: dcrutil.Amount(uint64(tspendAmount) - tspendFee),
Address: spendP2shAddr,
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// Generate a TAdd for some amount ensuring there's change and direct
// the change to a P2PKH addr.
taddAmount := int64(outs[0].Amount() / 2)
taddChange := taddAmount
taddFee := dcrutil.Amount(0)
tadd1 := g.CreateTreasuryTAddChange(&taddOut1, dcrutil.Amount(taddAmount),
taddFee, addP2pkhAddr)
tadd1.Version = wire.TxVersionTreasury
taddHash1 := tadd1.TxHash()
// Generate a second TAdd that pays the change to a unique P2SH script.
tadd2 := g.CreateTreasuryTAddChange(&outs[0], dcrutil.Amount(taddAmount),
taddFee, addP2shAddr)
tadd2.Version = wire.TxVersionTreasury
taddHash2 := tadd2.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to approve the tspend.
//
// ... -> bpretvi1 -> bv0 -> ... -> bvn
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("bv%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate a block that includes the tspend and tadd.
//
// ... -> bvn -> btspend
// ---------------------------------------------------------------------
var tspendHeight, tspendIndex uint32
g.NextBlock("btspend", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
tspendHeight = b.Header.Height
tspendIndex = uint32(len(b.STransactions))
b.AddSTransaction(tspend)
b.AddSTransaction(tadd1)
b.AddSTransaction(tadd2)
})
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
// Ensure the CFilter committed to the outputs of the TSpend and TAdds.
tipHash = &g.chain.BestSnapshot().Hash
bcf, err := g.chain.FilterByBlockHash(tipHash)
if err != nil {
t.Fatal(err)
}
bcfKey := blockcf2.Key(&(g.Tip().Header.MerkleRoot))
wantMatches := [][]byte{
tspend.TxOut[1].PkScript[1:], // P2PKH without OP_TGEN
tspend.TxOut[2].PkScript[1:], // P2SH without OP_TGEN
tadd1.TxOut[1].PkScript[1:], // P2PKH without OP_SSTxChange
tadd2.TxOut[1].PkScript[1:], // P2SH without OP_SSTxChange
}
for i, wantMatch := range wantMatches {
if !bcf.Match(bcfKey, wantMatch) {
t.Fatalf("bcf did not match script %d %x", i, wantMatch)
}
}
// Create a tx that spends from the TSPend outputs and the TAdd change
// output.
tx := wire.NewMsgTx()
tx.AddTxIn(&wire.TxIn{ // TSpend P2PKH output
PreviousOutPoint: wire.OutPoint{
Hash: tspendHash,
Index: 1,
Tree: 1,
},
Sequence: wire.MaxTxInSequenceNum,
ValueIn: tspendAmount,
BlockHeight: tspendHeight,
BlockIndex: tspendIndex,
})
tx.AddTxIn(&wire.TxIn{ // TSPend P2SH output
PreviousOutPoint: wire.OutPoint{
Hash: tspendHash,
Index: 2,
Tree: 1,
},
Sequence: wire.MaxTxInSequenceNum,
ValueIn: tspendAmount,
BlockHeight: tspendHeight,
BlockIndex: tspendIndex,
SignatureScript: append([]byte{txscript.OP_DATA_2}, spendP2shScript...),
})
tx.AddTxIn(&wire.TxIn{ // TAdd P2PKH output
PreviousOutPoint: wire.OutPoint{
Hash: taddHash1,
Index: 1,
Tree: 1,
},
Sequence: wire.MaxTxInSequenceNum,
ValueIn: taddChange,
BlockHeight: tspendHeight,
BlockIndex: tspendIndex + 1,
})
tx.AddTxIn(&wire.TxIn{ // TAdd P2SH output
PreviousOutPoint: wire.OutPoint{
Hash: taddHash2,
Index: 1,
Tree: 1,
},
Sequence: wire.MaxTxInSequenceNum,
ValueIn: taddChange,
BlockHeight: tspendHeight,
BlockIndex: tspendIndex + 2,
SignatureScript: append([]byte{txscript.OP_DATA_3}, addP2shScript...),
})
tx.AddTxOut(&wire.TxOut{
Version: 0,
Value: tspendAmount*2 + taddChange,
})
// Generate the valid signature for the first input, which is a P2PKH.
sig, err := sign.SignatureScript(tx, 0, tspend.TxOut[1].PkScript,
txscript.SigHashAll, spendPrivKey.Serialize(), dcrec.STEcdsaSecp256k1,
true)
if err != nil {
t.Fatalf("unable to generate sig: %v", err)
}
tx.TxIn[0].SignatureScript = sig
// Generate the valid signature for the third input, which is a P2PKH.
sig, err = sign.SignatureScript(tx, 2, tadd1.TxOut[1].PkScript,
txscript.SigHashAll, addPrivKey.Serialize(), dcrec.STEcdsaSecp256k1,
true)
if err != nil {
t.Fatalf("unable to generate sig: %v", err)
}
tx.TxIn[2].SignatureScript = sig
// ---------------------------------------------------------------------
// Generate up to the block where the tspend is spendable.
//
// ... -> btspend -> bpremat0 -> ... -> bprematn
// ---------------------------------------------------------------------
for i := uint32(0); i < uint32(params.CoinbaseMaturity-1); i++ {
name := fmt.Sprintf("bpremat%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate a block that includes the transaction spending from the
// TSpend and TAdd.
//
// ... -> btpremat -> btredeem
// ---------------------------------------------------------------------
g.NextBlock("btredeem", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
func TestTSpendDupVote(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create two TSPEND with invalid bits and duplicate votes.
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspendAmount := devsub * (tvi*mul - uint64(params.CoinbaseMaturity) +
uint64(start-nextBlockHeight))
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey,
[]chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
tspend2 := g.CreateTreasuryTSpend(privKey,
[]chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash2 := tspend2.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
// Generate votes up to TVI. This is legal however they should NOT be
// counted in the totals since they are outside of the voting window.
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Duplicate votes on the same treasury spend tx hash illegal and
// therefore the tx is NOT recognized as a vote.
//
// ... -> pretvi1
// \-> bdv0
// ---------------------------------------------------------------------
startTip := g.TipName()
voteCount := params.TicketsPerBlock
g.NextBlock("bdv0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t,
[]*chainhash.Hash{
&tspendHash,
&tspendHash,
},
[]stake.TreasuryVoteT{
stake.TreasuryVoteYes,
stake.TreasuryVoteYes,
},
voteCount, true))
g.RejectTipBlock(ErrBadTxInput)
// ---------------------------------------------------------------------
// Invalid treasury spend tx vote bits are illegal and therefore the tx
// is NOT recognized as a vote.
//
// ... -> pretvi1
// \-> bdv1
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("bdv1", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t,
[]*chainhash.Hash{
&tspendHash2,
},
[]stake.TreasuryVoteT{
0x04, // Invalid bits
},
voteCount, true))
g.RejectTipBlock(ErrBadTxInput)
}
func TestTSpendTooManyTSpend(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create two TSPEND with invalid bits and duplicate votes.
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspendAmount := devsub * (tvi*mul - uint64(params.CoinbaseMaturity) +
uint64(start-nextBlockHeight))
tspendFee := uint64(0)
maxTspends := 7
tspends := make([]*wire.MsgTx, maxTspends+1)
tspendHashes := make([]*chainhash.Hash, maxTspends+1)
tspendVotes := make([]stake.TreasuryVoteT, maxTspends+1)
for i := 0; i < maxTspends+1; i++ {
tspends[i] = g.CreateTreasuryTSpend(privKey,
[]chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
hash := tspends[i].TxHash()
tspendHashes[i] = &hash
tspendVotes[i] = stake.TreasuryVoteYes
}
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
// Generate votes up to TVI. This is legal however they should NOT be
// counted in the totals since they are outside of the voting window.
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// 8 votes is illegal and therefore the tx is NOT recognized as a vote.
//
// ... -> pretvi1
// \-> bdv0
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
g.NextBlock("bdv0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, tspendHashes, tspendVotes, voteCount, true))
g.RejectTipBlock(ErrBadTxInput)
}
func TestTSpendWindow(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create TSPEND in mempool
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + uint32(tvi*mul*4) + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspendAmount := devsub * (tvi*mul - uint64(params.CoinbaseMaturity) +
uint64(start-nextBlockHeight))
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
// Generate votes up to TVI. This is legal however they should NOT be
// counted in the totals since they are outside of the voting window.
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate a TVI worth of rewards and try to spend more.
//
// ... -> b0 ... -> b7
// \-> bvidaltoosoon0
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("b%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Try accepting TSpend from the future.
g.NextBlock("bvidaltoosoon0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrInvalidTSpendWindow)
}
// TestTSpendSignature verifies that both PI keys work and in addition that an
// invalid key is indeed rejected.
func TestTSpendSignature(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create TSPEND in mempool with key 1 and 2
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height //+ uint32(tvi*mul*4) + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspendAmount1 := uint64(devsub / 3)
tspendFee := uint64(0)
tspend1 := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount1 - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspend1Hash := tspend1.TxHash()
// tspend 2.
tspendAmount2 := uint64(devsub / 5)
tspend2 := g.CreateTreasuryTSpend(privKey2, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount2 - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspend2Hash := tspend2.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight-1; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate TVI*MUL of yes votes for both TSpends,
//
// ... -> bpretvi1 -> btvi0 -> ... -> btvi7
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("btvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t,
[]*chainhash.Hash{
&tspend1Hash,
&tspend2Hash,
},
[]stake.TreasuryVoteT{
stake.TreasuryVoteYes,
stake.TreasuryVoteYes,
},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert that we are on a TVI.
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
// Assert treasury balance
ts, err := getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if ts.balance != int64(devsub*tvi*mul) {
t.Fatalf("assert balance got %v want devsub %v", ts.balance,
devsub*tvi*mul)
}
// ---------------------------------------------------------------------
// Add tspend1 twice
//
// ... -> btvi7 ->
// \-> double0
// ---------------------------------------------------------------------
startTip := g.TipName()
g.NextBlock("double0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpends
b.AddSTransaction(tspend1)
b.AddSTransaction(tspend2)
b.AddSTransaction(tspend1)
})
g.RejectTipBlock(ErrDuplicateTx)
// ---------------------------------------------------------------------
// Add tspend1 and tspend2
//
// ... -> btvi7 -> doubleok0
// \-> double0
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("doubleok0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpends
b.AddSTransaction(tspend1)
b.AddSTransaction(tspend2)
})
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
// Assert treasury balance
ts, err = getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if ts.balance != int64(devsub*tvi*mul)+devsub {
t.Fatalf("assert balance got %v want devsub %v", ts.balance,
devsub*tvi*mul+devsub)
}
// ---------------------------------------------------------------------
// Add Coinbase maturity blocks and assert treasury balance.
//
// ... -> btvi7 -> doubleok0 -> cbm0 -> cbm1
// \-> double0
// ---------------------------------------------------------------------
for i := uint32(0); i < uint32(params.CoinbaseMaturity); i++ {
name := fmt.Sprintf("cbm%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert treasury balance
ts, err = getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
expectedBalance := int64(devsub*tvi*mul) +
(int64(devsub) * int64(params.CoinbaseMaturity+1)) // doubleok0+cbm0+cbm1
expectedBalance -= int64(tspendAmount1)
expectedBalance -= int64(tspendAmount2)
if ts.balance != expectedBalance {
t.Fatalf("assert balance got %v want devsub %v", ts.balance,
expectedBalance)
}
}
// TestTSpendSignatureInvalid verifies that a tspend is disallowed with an
// invalid key.
func TestTSpendSignatureInvalid(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create TSPEND in mempool.
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height //+ uint32(tvi*mul*4) + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
// Corrupt key for test.
wrongKey := make([]byte, len(privKey))
copy(wrongKey, privKey)
wrongKey[len(wrongKey)-1] = ^wrongKey[len(wrongKey)-1]
tspendAmount1 := uint64(devsub / 3)
tspendFee := uint64(0)
tspend1 := g.CreateTreasuryTSpend(wrongKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount1 - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspend1Hash := tspend1.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bva19 -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight-1; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate TVI*MUL of yes votes for TSpend,
//
// ... -> bpretvi1 -> btvi0 -> ... -> btvi7
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("btvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t,
[]*chainhash.Hash{
&tspend1Hash,
},
[]stake.TreasuryVoteT{
stake.TreasuryVoteYes,
},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// Assert that we are on a TVI.
if standalone.IsTreasuryVoteInterval(uint64(g.Tip().Header.Height), tvi) {
t.Fatalf("expected !TVI %v", g.Tip().Header.Height)
}
// Assert treasury balance
ts, err := getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if ts.balance != int64(devsub*tvi*mul) {
t.Fatalf("assert balance got %v want devsub %v", ts.balance,
devsub*tvi*mul)
}
// ---------------------------------------------------------------------
// Add tspend with invalid signature
//
// ... -> btvi7 ->
// \-> double0
// ---------------------------------------------------------------------
g.NextBlock("invalidsig0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend1)
})
g.RejectTipBlock(ErrUnknownPiKey)
}
func TestTSpendExists(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Shorter versions of useful params for convenience.
cbm := params.CoinbaseMaturity
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// splitSecondRegularTxOutputs is a munge function which modifies the
// provided block by replacing its second regular transaction with one
// that creates several utxos.
const splitTxNumOutputs = 6
splitSecondRegularTxOutputs := func(b *wire.MsgBlock) {
// Remove the current outputs of the second transaction while
// saving the relevant public key script, input amount, and fee
// for below.
tx := b.Transactions[1]
inputAmount := tx.TxIn[0].ValueIn
pkScript := tx.TxOut[0].PkScript
fee := inputAmount - tx.TxOut[0].Value
tx.TxOut = tx.TxOut[:0]
// Final outputs are the input amount minus the fee split into
// more than one output. These are intended to provide
// additional utxos for testing.
outputAmount := inputAmount - fee
splitAmount := outputAmount / splitTxNumOutputs
for i := 0; i < splitTxNumOutputs; i++ {
if i == splitTxNumOutputs-1 {
splitAmount = outputAmount -
splitAmount*(splitTxNumOutputs-1)
}
tx.AddTxOut(wire.NewTxOut(splitAmount, pkScript))
}
}
// Generate spendable outputs to do fork tests with.
var txOuts [][]chaingen.SpendableOut
genBlocks := cbm * 8
for i := uint16(0); i < genBlocks; i++ {
outs := g.OldestCoinbaseOuts()
name := fmt.Sprintf("bouts%v", i)
g.NextBlock(name, &outs[0], outs[1:], replaceTreasuryVersions,
replaceCoinbase, splitSecondRegularTxOutputs)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
souts := make([]chaingen.SpendableOut, 0, splitTxNumOutputs)
for j := 0; j < splitTxNumOutputs; j++ {
spendableOut := chaingen.MakeSpendableOut(g.Tip(), 1,
uint32(j))
souts = append(souts, spendableOut)
}
txOuts = append(txOuts, souts)
}
// ---------------------------------------------------------------------
// Create TSPEND in mempool
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspendAmount := uint64(devsub)
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
}}, dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// ---------------------------------------------------------------------
// Generate enough blocks to get to TVI.
//
// ... -> bouts -> bpretvi0 -> bpretvi1
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate two TVI with votes.
//
// ... -> b0 ... -> b7
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*2; i++ {
name := fmt.Sprintf("b%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Generate a TVI and mine same TSpend, which has to be rejected.
//
// ... -> be0 ... -> be3 ->
// \-> bexists0
// ---------------------------------------------------------------------
startTip := g.TipName()
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("be%v", i)
if i == 0 {
// Mine tspend.
g.NextBlock(name, nil, txOuts[i][1:],
replaceTreasuryVersions,
replaceCoinbase, func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
} else {
g.NextBlock(name, nil, txOuts[i][1:],
replaceTreasuryVersions,
replaceCoinbase)
}
g.AcceptTipBlock()
}
oldTip := g.TipName()
// Mine tspend again.
g.NextBlock("bexists0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrTSpendExists)
// ---------------------------------------------------------------------
// Generate a TVI and mine same TSpend, should not exist since it is a
// fork.
//
// /-> be0 ... -> be3 -> bexists0
// ... -> b3
// \-> bep0 ... -> bep3 -> bexists1
// ---------------------------------------------------------------------
g.SetTip(startTip)
var nextFork uint64
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("bep%v", i)
g.NextBlock(name, nil, txOuts[i][1:], replaceTreasuryVersions,
replaceCoinbase)
g.AcceptedToSideChainWithExpectedTip(oldTip)
nextFork = i + 1
}
// Mine tspend again.
g.NextBlock("bexists1", nil, txOuts[nextFork][1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.AcceptTipBlock()
// ---------------------------------------------------------------------
// Generate a TVI and mine same TSpend, should not exist since it is a
// fork.
//
// /-> be0 ... -> be3 -> bexists0
// ... -> b3
// \-> bep0 ... -> bep3 -> bexists1 ->bep4
// \-> bepp0 ... -> bepp3 -> bexists2 -> bxxx0 .. bxxx3
// ---------------------------------------------------------------------
// Generate one more block to extend current best chain.
name := fmt.Sprintf("bep%v", nextFork)
g.NextBlock(name, nil, txOuts[nextFork+1][1:], replaceTreasuryVersions,
replaceCoinbase)
g.AcceptTipBlock()
oldTip = g.TipName()
oldHeight := g.Tip().Header.Height
// Create bepp fork
g.SetTip(startTip)
txIdx := uint64(0)
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("bepp%v", i)
g.NextBlock(name, nil, txOuts[i][1:],
replaceTreasuryVersions, replaceCoinbase)
g.AcceptedToSideChainWithExpectedTip(oldTip)
txIdx = i + 1
}
// Mine tspend yet again.
g.NextBlock("bexists2", nil, txOuts[txIdx][1:],
replaceTreasuryVersions, replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.AcceptedToSideChainWithExpectedTip(oldTip)
txIdx++
// Force reorg
for i := uint64(0); i < tvi; i++ {
name := fmt.Sprintf("bxxx%v", i)
b := g.NextBlock(name, nil, txOuts[txIdx][1:],
replaceTreasuryVersions, replaceCoinbase)
if b.Header.Height <= oldHeight {
g.AcceptedToSideChainWithExpectedTip(oldTip)
} else {
g.AcceptTipBlock()
}
txIdx++
}
}
func TestTreasuryBalance(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create 10 blocks that has a tadd without change.
//
// ... -> b0
// ---------------------------------------------------------------------
blockCount := 10
expectedTotal := devsub *
(blockCount - int(params.CoinbaseMaturity)) // dev subsidy
skippedTotal := 0
for i := 0; i < blockCount; i++ {
amount := i + 1
if i < blockCount-int(params.CoinbaseMaturity) {
expectedTotal += amount
} else {
skippedTotal += amount
}
outs := g.OldestCoinbaseOuts()
name := fmt.Sprintf("b%v", i)
_ = g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TADD
tx := g.CreateTreasuryTAdd(&outs[0],
dcrutil.Amount(amount),
dcrutil.Amount(0))
tx.Version = wire.TxVersionTreasury
b.AddSTransaction(tx)
})
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
iterations := 1
ts, err := getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if ts.balance != int64(expectedTotal) {
t.Fatalf("invalid balance: total %v expected %v",
ts.balance, expectedTotal)
}
if ts.values[1].amount != int64(blockCount) {
t.Fatalf("invalid Value: total %v expected %v",
ts.values[0], int64(blockCount))
}
// ---------------------------------------------------------------------
// Create 10 blocks that has a tadd with change. Pretend that the TSpend
// transaction is in the mempool and vote on it.
//
// ... -> b10
// ---------------------------------------------------------------------
// This looks a little funky but it was coppied from the prior TSPEND
// test that created this many tspends. Since that is no longer
// possible use the for loop to get to the same totals.
var tspendAmount, tspendFee int
nextTipHeight := int64(g.Tip().Header.Height + 1)
expiry := standalone.CalcTSpendExpiry(nextTipHeight,
params.TreasuryVoteInterval,
params.TreasuryVoteIntervalMultiplier)
for i := 0; i < blockCount*2+int(params.CoinbaseMaturity); i++ {
if i > (blockCount * 2) {
// Skip last CoinbaseMaturity blocks.
break
}
tspendAmount += i + 1
tspendFee++
}
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{
{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
},
},
dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// Treasury votes munger.
addTSpendVotes := func(b *wire.MsgBlock) {
// Find SSGEN and append Yes vote.
for k, v := range b.STransactions {
if !stake.IsSSGen(v, yesTreasury) {
continue
}
if len(v.TxOut) != 3 {
t.Fatalf("expected SSGEN.TxOut len 3 got %v",
len(v.TxOut))
}
// Append vote: OP_RET OP_DATA <TV> <tspend hash> <vote bits>
vote := make([]byte, 2+chainhash.HashSize+1)
vote[0] = 'T'
vote[1] = 'V'
copy(vote[2:], tspendHash[:])
vote[len(vote)-1] = 0x01 // Yes
s, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).
AddData(vote).Script()
if err != nil {
t.Fatal(err)
}
b.STransactions[k].TxOut = append(b.STransactions[k].TxOut,
&wire.TxOut{
PkScript: s,
})
b.STransactions[k].Version = wire.TxVersionTreasury
// Assert vote insertion worked.
_, err = stake.GetSSGenTreasuryVotes(s)
if err != nil {
t.Fatalf("expected treasury vote: %v", err)
}
// Assert this remains a valid SSGEN.
err = stake.CheckSSGen(b.STransactions[k], yesTreasury)
if err != nil {
t.Fatalf("expected SSGen: %v", err)
}
}
}
expectedTotal += skippedTotal
expectedTotal += devsub * blockCount // dev subsidy
for i := blockCount; i < blockCount*2; i++ {
amount := i + 1
if i < (blockCount*2)-int(params.CoinbaseMaturity) {
expectedTotal += amount
}
outs := g.OldestCoinbaseOuts()
name := fmt.Sprintf("b%v", i)
_ = g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes,
func(b *wire.MsgBlock) {
tx := g.CreateTreasuryTAdd(&outs[0],
dcrutil.Amount(amount),
dcrutil.Amount(1))
tx.Version = wire.TxVersionTreasury
b.AddSTransaction(tx)
})
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
iterations += 1
ts, err = getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
if ts.balance != int64(expectedTotal) {
t.Fatalf("invalid balance: total %v expected %v",
ts.balance, expectedTotal)
}
if ts.values[1].amount != int64(blockCount*2) {
t.Fatalf("invalid Value: total %v expected %v",
ts.values[0], int64(blockCount)*2)
}
// ---------------------------------------------------------------------
// Create 20 blocks that has a tspend and params.CoinbaseMaturity more
// to bring treasury balance back to 0.
//
// ... -> b20
// ---------------------------------------------------------------------
var doneTSpend bool
for i := 0; i < blockCount*2+int(params.CoinbaseMaturity); i++ {
outs := g.OldestCoinbaseOuts()
name := fmt.Sprintf("b%v", i+blockCount*2)
if (g.Tip().Header.Height+1)%4 == 0 && !doneTSpend {
// Insert TSPEND
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
doneTSpend = true
} else {
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
}
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
iterations += 2 // We generate 2*blockCount
ts, err = getTreasuryState(g, g.Tip().BlockHash())
if err != nil {
t.Fatal(err)
}
expected := int64(devsub*blockCount*iterations - tspendFee) // Expected devsub
if ts.balance != expected {
t.Fatalf("invalid balance: total %v expected %v",
ts.balance, expected)
}
}
// newTxOut returns a new transaction output with the given parameters.
func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut {
return &wire.TxOut{
Value: amount,
Version: pkScriptVer,
PkScript: pkScript,
}
}
func createTAdd(spend chaingen.SpendableOut, changeDivisor dcrutil.Amount, params *chaincfg.Params) func(b *wire.MsgBlock) {
redeemScript := []byte{txscript.OP_TRUE}
p2shOpTrueAddr, err := stdaddr.NewAddressScriptHashV0(redeemScript, params)
if err != nil {
panic(err)
}
return func(b *wire.MsgBlock) {
amount := spend.Amount()
tx := wire.NewMsgTx()
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: spend.PrevOut(),
Sequence: wire.MaxTxInSequenceNum,
ValueIn: int64(amount),
BlockHeight: spend.BlockHeight(),
BlockIndex: spend.BlockIndex(),
SignatureScript: []byte{txscript.OP_DATA_1, txscript.OP_TRUE},
})
// Add negative change.
trsyAddScript := []byte{txscript.OP_TADD}
if changeDivisor != 0 {
changeScriptVer, changeScript := p2shOpTrueAddr.StakeChangeScript()
change := spend.Amount() / changeDivisor
amount := spend.Amount() - change
tx.AddTxOut(wire.NewTxOut(int64(amount), trsyAddScript))
tx.AddTxOut(newTxOut(int64(change), changeScriptVer, changeScript))
} else {
tx.AddTxOut(wire.NewTxOut(int64(amount), trsyAddScript))
}
tx.Version = wire.TxVersionTreasury
b.AddSTransaction(tx)
}
}
func TestTAddCorners(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
cbm := params.CoinbaseMaturity
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with the
// treasury agenda version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// splitSecondRegularTxOutputs is a munge function which modifies the
// provided block by replacing its second regular transaction with one
// that creates several utxos.
const splitTxNumOutputs = 6
splitSecondRegularTxOutputs := func(b *wire.MsgBlock) {
// Remove the current outputs of the second transaction while
// saving the relevant public key script, input amount, and fee
// for below.
tx := b.Transactions[1]
inputAmount := tx.TxIn[0].ValueIn
pkScript := tx.TxOut[0].PkScript
fee := inputAmount - tx.TxOut[0].Value
tx.TxOut = tx.TxOut[:0]
// Final outputs are the input amount minus the fee split into
// more than one output. These are intended to provide
// additional utxos for testing.
outputAmount := inputAmount - fee
splitAmount := outputAmount / splitTxNumOutputs
for i := 0; i < splitTxNumOutputs; i++ {
if i == splitTxNumOutputs-1 {
splitAmount = outputAmount -
splitAmount*(splitTxNumOutputs-1)
}
tx.AddTxOut(wire.NewTxOut(splitAmount, pkScript))
}
}
// Generate spendable outputs to do fork tests with.
var txOuts [][]chaingen.SpendableOut
genBlocks := cbm * 8
for i := uint16(0); i < genBlocks; i++ {
outs := g.OldestCoinbaseOuts()
name := fmt.Sprintf("bouts%v", i)
g.NextBlock(name, &outs[0], outs[1:], replaceTreasuryVersions,
replaceCoinbase, splitSecondRegularTxOutputs)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
souts := make([]chaingen.SpendableOut, 0, splitTxNumOutputs)
for j := 0; j < splitTxNumOutputs; j++ {
spendableOut := chaingen.MakeSpendableOut(g.Tip(), 1,
uint32(j))
souts = append(souts, spendableOut)
}
txOuts = append(txOuts, souts)
}
// ---------------------------------------------------------------------
// Create TAdd with negative change.
//
// ... -> bn0
// ---------------------------------------------------------------------
mungeValueChange := func(b *wire.MsgBlock) {
for k := range b.STransactions {
if !stake.IsTAdd(b.STransactions[k]) {
continue
}
b.STransactions[k].TxOut[1].Value =
-b.STransactions[k].TxOut[1].Value
break
}
}
startTip := g.TipName()
g.NextBlock("bn0", nil, txOuts[0][1:], replaceTreasuryVersions,
replaceCoinbase,
createTAdd(txOuts[0][0], 10, params),
mungeValueChange)
g.RejectTipBlock(ErrBadTxOutValue)
// ---------------------------------------------------------------------
// Create TAdd with negative in amount.
//
// ... -> bn1
// ---------------------------------------------------------------------
mungeValueIn := func(b *wire.MsgBlock) {
for k := range b.STransactions {
if !stake.IsTAdd(b.STransactions[k]) {
continue
}
b.STransactions[k].TxIn[0].ValueIn =
-b.STransactions[k].TxIn[0].ValueIn
break
}
}
g.SetTip(startTip)
g.NextBlock("bn1", nil, txOuts[0][1:], replaceTreasuryVersions,
replaceCoinbase,
createTAdd(txOuts[0][0], 10, params),
mungeValueIn)
g.RejectTipBlock(ErrFraudAmountIn)
// ---------------------------------------------------------------------
// Create TAdd with negative out amount.
//
// ... -> bn2
// ---------------------------------------------------------------------
mungeValueOut := func(b *wire.MsgBlock) {
for k := range b.STransactions {
if !stake.IsTAdd(b.STransactions[k]) {
continue
}
b.STransactions[k].TxOut[0].Value =
-b.STransactions[k].TxOut[0].Value
break
}
}
g.SetTip(startTip)
g.NextBlock("bn2", nil, txOuts[0][1:], replaceTreasuryVersions,
replaceCoinbase,
createTAdd(txOuts[0][0], 10, params),
mungeValueOut)
g.RejectTipBlock(ErrBadTxOutValue)
// ---------------------------------------------------------------------
// Create TAdd with 0 change.
//
// ... -> bn3
// ---------------------------------------------------------------------
mungeChangeValue := func(b *wire.MsgBlock) {
for k := range b.STransactions {
if !stake.IsTAdd(b.STransactions[k]) {
continue
}
b.STransactions[k].TxOut[1].Value = 0
break
}
}
g.SetTip(startTip)
g.NextBlock("bn3", nil, txOuts[0][1:], replaceTreasuryVersions,
replaceCoinbase,
createTAdd(txOuts[0][0], 10, params),
mungeChangeValue)
g.RejectTipBlock(ErrInvalidTAddChange)
_ = tvi
_ = mul
}
func TestTreasuryBaseCorners(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the provided
// block by replacing the block, stake, and vote versions with the treasury
// deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// removeAllButTreasurybase is a munge function which modifies the provided
// block by removing all stake transactions other than the treasurybase.
removeAllButTreasurybase := func(b *wire.MsgBlock) {
b.Header.FreshStake = 0
b.STransactions = b.STransactions[:1]
}
// removeStakeTxns is a munge function which modifies the provided block by
// removing all stake transactions other than votes.
removeStakeTxns := func(b *wire.MsgBlock) {
b.Header.FreshStake = 0
b.Header.Revocations = 0
b.STransactions = b.STransactions[1:6]
}
// dupTreasurybase is a munge function which modifies the provided block by
// adding a duplicate treasurybase at the end of the stake tree.
dupTreasurybase := func(b *wire.MsgBlock) {
txCopy := b.STransactions[0].Copy()
txCopy.TxOut[1].PkScript[len(txCopy.TxOut[1].PkScript)-1] ^= 0x55
b.AddSTransaction(txCopy)
}
// flipTreasurybase is a munge function which modifies the provided block by
// swapping the treasurybase with the second transaction in the stake tree.
flipTreasurybase := func(b *wire.MsgBlock) {
stakeTxns := b.STransactions
stakeTxns[0], stakeTxns[1] = stakeTxns[1], stakeTxns[0]
}
// removeTreasurybase is a munge function which modifies the provided block
// by removing the treasurybase from the stake tree.
removeTreasurybase := func(b *wire.MsgBlock) {
b.STransactions = b.STransactions[1:]
}
// changeHeightTreasurybase is a munge function which modifies the provided
// block by adding one to the height encoded in the treasurybase.
changeHeightTreasurybase := func(b *wire.MsgBlock) {
b.STransactions[0].TxOut[1].PkScript[2]++
}
// corruptLengthTreasurybase is a munge function which modifies the provided
// block by mutating the treasurybase script to corrupt it.
corruptLengthTreasurybase := func(b *wire.MsgBlock) {
s := b.STransactions[0].TxOut[1].PkScript
b.STransactions[0].TxOut[1].PkScript = s[:len(s)-1]
}
// corruptTreasurybaseValueIn is a munge function which modifies the
// provided block by mutating the treasurybase in value.
corruptTreasurybaseValueIn := func(b *wire.MsgBlock) {
b.STransactions[0].TxIn[0].ValueIn -= 1
}
// corruptTreasurybaseValueOut is a munge function which modifies the
// provided block by mutating the treasurybase out value.
corruptTreasurybaseValueOut := func(b *wire.MsgBlock) {
b.STransactions[0].TxOut[0].Value -= 1
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Append a treasurybase to STransactions.
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
startTip := g.TipName()
g.NextBlock("twotb0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, dupTreasurybase)
g.RejectTipBlock(ErrMultipleTreasurybases)
// ---------------------------------------------------------------------
// Flip STransactions 0 and 1.
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("notbon0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, flipTreasurybase)
g.RejectTipBlock(ErrFirstTxNotTreasurybase)
// ---------------------------------------------------------------------
// No treasurybase.
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("notb0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, removeTreasurybase)
g.RejectTipBlock(ErrFirstTxNotTreasurybase)
// ---------------------------------------------------------------------
// No treasurybase and no tickets.
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("nothing0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, removeStakeTxns)
g.RejectTipBlock(ErrFirstTxNotTreasurybase)
// ---------------------------------------------------------------------
// Treasury base invalid height
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("height0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, changeHeightTreasurybase)
g.RejectTipBlock(ErrTreasurybaseHeight)
// ---------------------------------------------------------------------
// Treasury base invalid OP_RETURN script length.
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("length0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, corruptLengthTreasurybase)
g.RejectTipBlock(ErrFirstTxNotTreasurybase)
// ---------------------------------------------------------------------
// Only treasury base.
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("novotes0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, removeAllButTreasurybase)
g.RejectTipBlock(ErrVotesMismatch)
// ---------------------------------------------------------------------
// Treasury base invalid TxIn[0].Value
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("invalidin0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, corruptTreasurybaseValueIn)
g.RejectTipBlock(ErrBadTreasurybaseAmountIn)
// ---------------------------------------------------------------------
// Treasury base invalid TxOut[0].Value
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("invalidout0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, corruptTreasurybaseValueOut)
g.RejectTipBlock(ErrTreasurybaseOutValue)
// Note we can't hit the following errors in consensus:
// * ErrFirstTxNotTreasurybase (missing OP_RETURN)
// * ErrFirstTxNotTreasurybase (version)
// * ErrTreasurybaseTxNotOpReturn
// * ErrInvalidTreasurybaseTxOutputs
// * ErrInvalidTreasurybaseVersion
// * ErrInvalidTreasurybaseScript
}
func TestTSpendCorners(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the provided
// block by replacing the block, stake, and vote versions with the treasury
// deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Create TSpend in mempool
// ---------------------------------------------------------------------
nextBlockHeight := g.Tip().Header.Height + 1
tspendAmount := devsub
tspendFee := 100
expiry := standalone.CalcTSpendExpiry(int64(nextBlockHeight), tvi, mul)
start, _, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
}}, dcrutil.Amount(tspendFee), expiry)
tspendHash := tspend.TxHash()
// ---------------------------------------------------------------------
// Get to TVI
// ... -> pretvi1
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < start-nextBlockHeight; i++ {
name := fmt.Sprintf("bpretvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Vote on TSpend
// pretvi1 -> btvi0 -> ... -> btvi7
// ---------------------------------------------------------------------
voteCount := params.TicketsPerBlock
for i := uint64(0); i < tvi*mul; i++ {
name := fmt.Sprintf("btvi%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
}
// ---------------------------------------------------------------------
// Mine TSpend with improperly encoded ValueIn.
// -> btvi7
// \-> btspend0
// ---------------------------------------------------------------------
tspend.TxIn[0].ValueIn++
g.NextBlock("btspend0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrInvalidTSpendValueIn)
}
func TestTSpendFirstTVICorner(t *testing.T) {
// Clone the parameters so they can be mutated and remove the deployment
// for the treasury agenda to activate it.
const tVoteID = chaincfg.VoteIDTreasury
params := chaincfg.RegNetParams()
params = cloneParams(params)
removeDeployment(params, tVoteID)
// Change params to hit corner case.
params.StakeValidationHeight = 144
params.TicketPoolSize = 144
params.TreasuryVoteInterval = 11
params.TreasuryVoteIntervalMultiplier = 1
// Shorter versions of useful params for convenience.
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
svh := params.StakeValidationHeight
t.Logf("svh: %v tvi %v mul %v", svh, tvi, mul)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// With an SVH of 144 and a TVI of 11 and a MUL of 1 create a tspend
// that expires on block 156 (voting interval [143,154]). Start voting
// on block 144 (SVH) up to 153 (inclusive) which is 10*5 votes of the
// total of 55 possible. That easily is voted in since 60% is 33 yes
// votes.
expiry := standalone.CalcTSpendExpiry(140, tvi, mul)
start, end, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
t.Logf("tspend expiry %v start %v end %v", expiry, start, end)
tspend := newFakeCreateTSpend(privKey, []dcrutil.Amount{100000000}, 1,
expiry)
tspendHash := tspend.TxHash()
_, _, err = stake.CheckTSpend(tspend)
if err != nil {
t.Fatal(err)
}
// ---------------------------------------------------------------------
// Block One.
// ---------------------------------------------------------------------
// Add the required first block.
//
// genesis -> bfb
g.CreateBlockOne("bfb", 0)
g.AssertTipHeight(1)
g.AcceptTipBlock()
// Assert treasury agenda is indeed active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Generate enough blocks to have mature coinbase outputs to work with.
// Note that going forward all coinbase is munged to also output
// treasurybase.
//
// genesis -> bfb -> bm0 -> bm1 -> ... -> bm#
// ---------------------------------------------------------------------
ticketsPerBlock := params.TicketsPerBlock
coinbaseMaturity := params.CoinbaseMaturity
stakeEnabledHeight := params.StakeEnabledHeight
stakeValidationHeight := params.StakeValidationHeight
for i := uint16(0); i < coinbaseMaturity; i++ {
blockName := fmt.Sprintf("bm%d", i)
g.NextBlock(blockName, nil, nil, replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
g.AssertTipHeight(uint32(coinbaseMaturity) + 1)
// ---------------------------------------------------------------------
// Generate enough blocks to reach the stake enabled height while
// creating ticket purchases that spend from the coinbases matured
// above. This will also populate the pool of immature tickets.
//
// ... -> bm# ... -> bse0 -> bse1 -> ... -> bse#
// ---------------------------------------------------------------------
var ticketsPurchased int
for i := int64(0); int64(g.Tip().Header.Height) < stakeEnabledHeight; i++ {
outs := g.OldestCoinbaseOuts()
ticketOuts := outs[1:]
ticketsPurchased += len(ticketOuts)
blockName := fmt.Sprintf("bse%d", i)
g.NextBlock(blockName, nil, ticketOuts, replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
g.AssertTipHeight(uint32(stakeEnabledHeight))
// ---------------------------------------------------------------------
// Generate enough blocks to reach the stake validation height while
// continuing to purchase tickets using the coinbases matured above and
// allowing the immature tickets to mature and thus become live.
//
// ... -> bse# -> bsv0 -> bsv1 -> ... -> bsv#
// ---------------------------------------------------------------------
targetPoolSize := g.Params().TicketPoolSize * ticketsPerBlock
for i := int64(0); int64(g.Tip().Header.Height) < stakeValidationHeight-1; i++ {
// Only purchase tickets until the target ticket pool size is
// reached.
outs := g.OldestCoinbaseOuts()
ticketOuts := outs[1:]
if ticketsPurchased+len(ticketOuts) > int(targetPoolSize) {
ticketsNeeded := int(targetPoolSize) - ticketsPurchased
if ticketsNeeded > 0 {
ticketOuts = ticketOuts[1 : ticketsNeeded+1]
} else {
ticketOuts = nil
}
}
ticketsPurchased += len(ticketOuts)
blockName := fmt.Sprintf("bsv%d", i)
g.NextBlock(blockName, nil, ticketOuts, replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
g.AssertTipHeight(uint32(stakeValidationHeight - 1))
// ---------------------------------------------------------------------
// Test adding a tspend on the first possible height despite not being
// on a TVI.
//
// ... -> bse# -> bsv0 -> bsv1 -> ... -> bsv#
// \-> boink0
// ---------------------------------------------------------------------
startTip := g.TipName()
g.NextBlock("boink0", nil, nil, replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrNotTVI)
// ---------------------------------------------------------------------
// Start TSpend 'yes' voting here.
//
// ... -> bse# -> bsv0 -> bsv1 -> ... -> bsv# -> bsvv#
// \-> boink0
// ---------------------------------------------------------------------
// Full voting window -1 for block that contained no votes and another
// -1 because the TSpend can only be mined on a TVI.
numVotingBlocks := int64(tvi*mul - 2)
g.SetTip(startTip)
targetPoolSize = g.Params().TicketPoolSize * ticketsPerBlock
voteCount := params.TicketsPerBlock
for i := int64(0); int64(g.Tip().Header.Height) <
stakeValidationHeight+numVotingBlocks; i++ {
// Only purchase tickets until the target ticket pool size is
// reached.
outs := g.OldestCoinbaseOuts()
ticketOuts := outs[1:]
if ticketsPurchased+len(ticketOuts) > int(targetPoolSize) {
ticketsNeeded := int(targetPoolSize) - ticketsPurchased
if ticketsNeeded > 0 {
ticketOuts = ticketOuts[1 : ticketsNeeded+1]
} else {
ticketOuts = nil
}
}
ticketsPurchased += len(ticketOuts)
blockName := fmt.Sprintf("bsvv%d", i)
g.NextBlock(blockName, nil, ticketOuts, replaceCoinbase,
addTSpendVotes(t, []*chainhash.Hash{&tspendHash},
[]stake.TreasuryVoteT{stake.TreasuryVoteYes},
voteCount, false))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}
g.AssertTipHeight(uint32(stakeValidationHeight + numVotingBlocks))
// ---------------------------------------------------------------------
// Mine TSpend on TVI.
//
// ... -> bse# -> bsv0 -> bsv1 -> ... -> bsv# -> bsvv# ->
// \-> boink0 \-> boink1
// ---------------------------------------------------------------------
g.NextBlock("boink1", nil, nil,
replaceCoinbase,
func(b *wire.MsgBlock) {
// Add TSpend
b.AddSTransaction(tspend)
})
g.RejectTipBlock(ErrInvalidTVoteWindow)
}
func TestTreasuryInRegularTree(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the provided
// block by replacing the block, stake, and vote versions with the treasury
// deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// addTreasuryBaseRegular is a munge function which modifies the provided
// block to add a treasurybase to the regular transaction tree.
addTreasuryBaseRegular := func(b *wire.MsgBlock) {
txCopy := b.STransactions[0].Copy()
txCopy.TxOut[1].PkScript[len(txCopy.TxOut[1].PkScript)-1] ^= 0x55
b.AddTransaction(txCopy)
}
// moveTAddRegular is a munge function which modifies the provided block by
// moving a treasury add transaction from the stake transaction tree to the
// regular transaction tree.
moveTAddRegular := func(b *wire.MsgBlock) {
// Move TAdd to regular tree
tAddIdx := len(b.STransactions) - 1
b.Transactions = append(b.Transactions, b.STransactions[tAddIdx])
b.STransactions = b.STransactions[:tAddIdx]
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// ---------------------------------------------------------------------
// Append a treasurybase to transactions.
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
startTip := g.TipName()
g.NextBlock("tb0", nil, outs[1:], replaceTreasuryVersions, replaceCoinbase,
addTreasuryBaseRegular)
g.RejectTipBlock(ErrMultipleCoinbases)
// ---------------------------------------------------------------------
// Append tadd to transactions
// ---------------------------------------------------------------------
g.SetTip(startTip)
g.NextBlock("tadd0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, createTAdd(outs[0], 15, params), moveTAddRegular)
g.RejectTipBlock(ErrStakeTxInRegularTree)
// ---------------------------------------------------------------------
// Append TSpend to transactions.
// ---------------------------------------------------------------------
tspendAmount := uint64(devsub)
tspendFee := uint64(0)
tspend := g.CreateTreasuryTSpend(privKey, []chaingen.AddressAmountTuple{{
Amount: dcrutil.Amount(tspendAmount - tspendFee),
}}, dcrutil.Amount(tspendFee), 78) // Expiry check not hit.
g.SetTip(startTip)
g.NextBlock("tspend0", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, func(b *wire.MsgBlock) {
b.AddTransaction(tspend)
})
g.RejectTipBlock(ErrStakeTxInRegularTree)
}
func TestTSpendVoteCountSynthetic(t *testing.T) {
params := chaincfg.RegNetParams()
tmh := params.TicketMaturity + 2 + 2 // Genesis + block 1 + 2 to reach tvi
tvi := params.TreasuryVoteInterval
mul := params.TreasuryVoteIntervalMultiplier
tpb := params.TicketsPerBlock
expiry := standalone.CalcTSpendExpiry(int64(tmh), tvi, mul)
start, end, err := standalone.CalcTSpendWindow(expiry, tvi, mul)
if err != nil {
t.Fatal(err)
}
maxVotes := uint32(tpb) * (end - start)
quorum := uint64(maxVotes) * params.TreasuryVoteQuorumMultiplier /
params.TreasuryVoteQuorumDivisor
requiredVotes := uint64(maxVotes) *
params.TreasuryVoteRequiredMultiplier /
params.TreasuryVoteRequiredDivisor
// Create tspend.
tspend := newFakeCreateTSpend(privKey, []dcrutil.Amount{100000000}, 1,
expiry)
tspendHash := tspend.TxHash()
_, _, err = stake.CheckTSpend(tspend)
if err != nil {
t.Fatal(err)
}
// Create yes vote
yesVote := newFakeCreateVoteTx([]treasuryVoteTuple{{
tspendHash: tspendHash,
vote: 0x01, // Yes
}})
_, err = stake.CheckSSGenVotes(yesVote, yesTreasury)
if err != nil {
t.Fatal(err)
}
// Create no vote
noVote := newFakeCreateVoteTx([]treasuryVoteTuple{{
tspendHash: tspendHash,
vote: 0x02, // No
}})
_, err = stake.CheckSSGenVotes(noVote, yesTreasury)
if err != nil {
t.Fatal(err)
}
fakeTreasuryBase := &wire.MsgTx{}
var ticketCount int // Overall ticket counter if needed in tests
tests := []struct {
name string
numNodes int64
set func(*BlockChain, *blockNode)
blockVersion int32
expectedYesVotes int
expectedNoVotes int
failure bool
}{{
name: "All yes",
numNodes: int64(end),
expectedYesVotes: int(tpb) * int(tvi*mul),
expectedNoVotes: 0,
failure: false,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
yesVote,
yesVote,
yesVote,
yesVote,
yesVote,
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "All no",
numNodes: int64(end),
expectedYesVotes: 0,
expectedNoVotes: int(tpb) * int(tvi*mul),
failure: true,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
noVote,
noVote,
noVote,
noVote,
noVote,
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "All abstain",
numNodes: int64(end),
expectedYesVotes: 0,
expectedNoVotes: 0,
failure: true,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) && node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "Undervote one tvi",
numNodes: int64(end),
expectedYesVotes: 0,
expectedNoVotes: 0,
failure: true,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
x := uint32(tvi) // subtract one tvi
if node.height >= int64(start-x) &&
node.height < int64(end-x) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "Overvote",
numNodes: int64(end),
expectedYesVotes: 0,
expectedNoVotes: 0,
failure: true,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
x := uint32(tvi) // add one tvi
if node.height >= int64(start+x) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "All yes quorum",
numNodes: int64(end),
expectedYesVotes: int(quorum),
expectedNoVotes: 0,
failure: false,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount >= int(quorum) {
break
}
mblk.STransactions = append(mblk.STransactions,
yesVote)
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "All yes quorum - 1",
numNodes: int64(end),
expectedYesVotes: int(quorum - 1),
expectedNoVotes: 0,
failure: true,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount >= int(quorum-1) {
break
}
mblk.STransactions = append(mblk.STransactions,
yesVote)
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "All yes quorum + 1",
numNodes: int64(end),
expectedYesVotes: int(quorum + 1),
expectedNoVotes: 0,
failure: false,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount >= int(quorum+1) {
break
}
mblk.STransactions = append(mblk.STransactions,
yesVote)
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "Exactly yes required",
numNodes: int64(end),
expectedYesVotes: int(requiredVotes),
expectedNoVotes: int(maxVotes) - int(requiredVotes),
failure: false,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount >= int(requiredVotes) {
mblk.STransactions = append(mblk.STransactions,
noVote)
} else {
mblk.STransactions = append(mblk.STransactions,
yesVote)
}
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "Yes required - 1",
numNodes: int64(end),
expectedYesVotes: int(requiredVotes - 1),
expectedNoVotes: int(maxVotes) - int(requiredVotes-1),
failure: true,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount < int(requiredVotes-1) {
mblk.STransactions = append(mblk.STransactions,
yesVote)
} else {
mblk.STransactions = append(mblk.STransactions,
noVote)
}
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "Yes required + 1",
numNodes: int64(end),
expectedYesVotes: int(requiredVotes + 1),
expectedNoVotes: int(maxVotes) - int(requiredVotes+1),
failure: false,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount < int(requiredVotes+1) {
mblk.STransactions = append(mblk.STransactions,
yesVote)
} else {
mblk.STransactions = append(mblk.STransactions,
noVote)
}
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}, {
name: "Exactly yes required with abstain",
numNodes: int64(end),
expectedYesVotes: int(requiredVotes),
expectedNoVotes: 0,
failure: false,
set: func(bc *BlockChain, node *blockNode) {
// Create block.
bh := node.Header()
mblk := wire.NewMsgBlock(&bh)
// Append SSGen
if node.height >= int64(start) &&
node.height < int64(end) {
mblk.STransactions = []*wire.MsgTx{
fakeTreasuryBase,
}
for i := 0; i < int(tpb); i++ {
if ticketCount >= int(requiredVotes) {
// Do nothing.
} else {
mblk.STransactions = append(mblk.STransactions,
yesVote)
}
ticketCount++
}
}
// Insert block into the cache.
bc.addRecentBlock(dcrutil.NewBlock(mblk))
},
}}
for _, test := range tests {
// Create new BlockChain in order to blow away cache. Also notice the
// cache needs to be large enough to hold all of the blocks for these
// tests without eviction.
bc := newFakeChain(params)
node := bc.bestChain.Tip()
bc.recentBlocks = lru.NewKVCache(uint(test.numNodes))
ticketCount = 0
for i := int64(0); i < test.numNodes; i++ {
// Version 9 is regnet decentralized treasury agenda.
node = newFakeNode(node, test.blockVersion,
9, 0, time.Now())
// Override version.
if test.set != nil {
test.set(bc, node)
}
bc.index.AddNode(node)
bc.bestChain.SetTip(node)
}
if !standalone.IsTreasuryVoteInterval(uint64(node.height), tvi) {
t.Fatalf("Not a TVI: %v", node.height)
}
tv, err := bc.tSpendCountVotes(node.parent, dcrutil.NewTx(tspend))
if err != nil {
t.Fatal(err)
}
if tv.start != start {
t.Fatalf("invalid start want %v got %v", start, start)
}
if tv.end != end {
t.Fatalf("invalid end want %v got %v", end, end)
}
if tv.yes != test.expectedYesVotes {
t.Fatalf("invalid yes want %v got %v",
test.expectedYesVotes, tv.yes)
}
if tv.no != test.expectedNoVotes {
t.Fatalf("invalid no want %v got %v",
test.expectedNoVotes, tv.no)
}
// Check error
err = bc.checkTSpendHasVotes(node.parent, dcrutil.NewTx(tspend))
if test.failure {
if err == nil {
t.Fatalf("expected failure")
}
} else {
if err != nil {
t.Fatalf("%v: %v", test.name, err)
}
}
}
}
// TestTSpendTooManyTAdds tests that a block that contains more TAdds than
// allowed by consensus rules is rejected while a block containing exactly the
// maximum amount is accepted.
func TestTSpendTooManyTAdds(t *testing.T) {
// Use a set of test chain parameters which allow for quicker vote
// activation as compared to various existing network params.
params := quickVoteActivationParams()
// Clone the parameters so they can be mutated, find the correct
// deployment for the treasury agenda, and, finally, ensure it is
// always available to vote by removing the time constraints to prevent
// test failures when the real expiration time passes.
const tVoteID = chaincfg.VoteIDTreasury
params = cloneParams(params)
tVersion, deployment, err := findDeployment(params, tVoteID)
if err != nil {
t.Fatal(err)
}
removeDeploymentTimeConstraints(deployment)
// Create a test harness initialized with the genesis block as the tip.
g, teardownFunc := newChaingenHarness(t, params, "treasurytest")
defer teardownFunc()
// replaceTreasuryVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with
// the treasury deployment version.
replaceTreasuryVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(int32(tVersion))(b)
chaingen.ReplaceStakeVersion(tVersion)(b)
chaingen.ReplaceVoteVersions(tVersion)(b)
}
// addTAdds is a helper function that returns a munge function that
// includes the specified tadds in the stake tree of a block.
addTAdds := func(tadds []*wire.MsgTx) func(b *wire.MsgBlock) {
return func(b *wire.MsgBlock) {
for _, tx := range tadds {
b.AddSTransaction(tx)
}
}
}
// ---------------------------------------------------------------------
// Generate and accept enough blocks with the appropriate vote bits set
// to reach one block prior to the treasury agenda becoming active.
// ---------------------------------------------------------------------
g.AdvanceToStakeValidationHeight()
g.AdvanceFromSVHToActiveAgendas(tVoteID)
// Ensure treasury agenda is active.
tipHash := &g.chain.BestSnapshot().Hash
gotActive, err := g.chain.IsTreasuryAgendaActive(tipHash)
if err != nil {
t.Fatalf("IsTreasuryAgendaActive: %v", err)
}
if !gotActive {
t.Fatalf("IsTreasuryAgendaActive: expected enabled treasury")
}
// The maximum number of tadds is hardcoded here instead of using a
// global const so that any code changes that unintentionally cause
// this maximum to change in blockchain code are flagged by this test.
maxNbTadds := 20
// ---------------------------------------------------------------------
// Generate enough blocks to have spendable outputs to create the
// required number of tadds. Due to having lowered the coinbase
// maturity, we'll need to manually generate and store these outputs
// until we have enough.
//
// ... -> bout0 -> .. boutn
// ---------------------------------------------------------------------
neededOuts := maxNbTadds + 1
var oldOuts []chaingen.SpendableOut
outs := g.OldestCoinbaseOuts()
for i := uint32(0); i < uint32(neededOuts); i++ {
name := fmt.Sprintf("bout%v", i)
g.NextBlock(name, nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase)
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
outs = g.OldestCoinbaseOuts()
oldOuts = append(oldOuts, outs[0])
}
// Create more than the maximum allowed number of TAdds.
taddAmount := int64(1701)
taddFee := dcrutil.Amount(0)
tadds := make([]*wire.MsgTx, 0, maxNbTadds+1)
for i := 0; i < maxNbTadds+1; i++ {
tadd := g.CreateTreasuryTAdd(&oldOuts[i], dcrutil.Amount(taddAmount), taddFee)
tadd.Version = wire.TxVersionTreasury
tadds = append(tadds, tadd)
}
// ---------------------------------------------------------------------
// Attempt to mine a block with an invalid number of tadds.
//
// ... -> boutn -> btoomanytadds
// ---------------------------------------------------------------------
tipName := g.TipName()
g.NextBlock("btoomanytadds", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, addTAdds(tadds))
g.SaveTipCoinbaseOutsWithTreasury()
g.RejectTipBlock(ErrTooManyTAdds)
// ---------------------------------------------------------------------
// Mine a block with the maximum number of tadds.
//
// ... -> boutn -> btoomanytadds
// \-> bmaxtadds
// ---------------------------------------------------------------------
g.SetTip(tipName)
g.NextBlock("bmaxtadds", nil, outs[1:], replaceTreasuryVersions,
replaceCoinbase, addTAdds(tadds[1:]))
g.SaveTipCoinbaseOutsWithTreasury()
g.AcceptTipBlock()
}