dcrd/blockchain/treasury_test.go
Dave Collins 974f80d1e5
multi: Consolidate header proof logic.
Currently, the p2p and rpcserver handlers that deal with compact filters
essentially duplicate the logic for dealing with the associated header
proof and are also required to understand the structure of the proof in
the form of knowing the associated index.  This is not ideal since there
is no guarantee that a given index will always be the same through
consensus changes.

With those points in mind, this reworks the header proof logic slightly
to consolidate it by introducing a new HeaderProof struct to the
blockchain that houses a header commitment inclusion proof and
associated proof index and updating the FilterByBlockHash method to
return the header proof along with the filter and updates the p2p and
rpcserver handlers and associated interfaces and tests accordingly.

The result is that the aforementioned p2p and rpcserver handlers for
obtaining compact filters no longer need to understand anything about
the proofs.  It is also useful for any future header commitments too
since the same logic will hold true for those instances as well.
2022-05-14 17:28:50 -05:00

4296 lines
137 KiB
Go

// Copyright (c) 2020-2022 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"
"reflect"
"testing"
"time"
"github.com/decred/dcrd/blockchain/stake/v5"
"github.com/decred/dcrd/blockchain/standalone/v2"
"github.com/decred/dcrd/blockchain/v5/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/v4/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.
dbPath := t.TempDir()
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.
dbPath := t.TempDir()
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) {
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])
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 := newChaingenHarness(t, params)
// 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 := g.chain.db.View(func(dbTx database.Tx) error {
var err error
tsr, err = dbFetchTreasuryBalance(dbTx, hash)
return err
})
return tsr, err
}
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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) {
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])
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++
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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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--
}
// 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--
}
// ---------------------------------------------------------------------
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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 := newChaingenHarness(t, params)
// 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)
if err != nil {
t.Fatal(err)
}
// Create no vote
noVote := newFakeCreateVoteTx([]treasuryVoteTuple{{
tspendHash: tspendHash,
vote: 0x02, // No
}})
_, err = stake.CheckSSGenVotes(noVote)
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 := newChaingenHarness(t, params)
// 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()
}