[release-v1.4] multi: Enable vote for DCP0004.

This implements the agenda for voting on the sequence lock fixes as
defined in DCP0004 along with consensus tests and mempool acceptance
tests to ensure its correctness.

It also modifies the mempool to conditionally treat all transactions
with enabled sequence locks as non standard until the vote passes at
which point the will become standard with the modified semantics
enforced.

The following is an overview of the changes:

- Generate new version blocks and reject old version blocks after a
  super majority has been reached
  - New block version on mainnet is version 6
  - New block version on testnet is version 7
- Introduce a convenience function for determining if the vote passed
  and is now active
- Enforce modified sequence lock semantics in accordance with the state
  of the vote
- Modify the more strict standardness checks (acceptance to the mempool
  and relay) to enforce DCP0004 in accordance with the state of the vote
  - Make all transactions with enabled sequence locks non standard until
    the agenda vote passes
  - Add tests to ensure acceptance and relay behave according to the
    aforementioned description
- Add tests for determining if the agenda is active for both mainnet and
  testnet
- Add tests to ensure the corrected sequence locks are handled properly
  depending on the result of the vote
This commit is contained in:
Dave Collins 2019-01-26 08:49:35 -06:00
parent ae3d60847f
commit f81e19783c
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
7 changed files with 1126 additions and 32 deletions

View File

@ -1,17 +1,21 @@
// Copyright (c) 2017-2018 The Decred developers
// Copyright (c) 2017-2019 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 (
"fmt"
"math"
"testing"
"time"
"github.com/decred/dcrd/blockchain/chaingen"
"github.com/decred/dcrd/blockchain/stake"
"github.com/decred/dcrd/chaincfg"
"github.com/decred/dcrd/dcrutil"
"github.com/decred/dcrd/txscript"
"github.com/decred/dcrd/wire"
)
// testLNFeaturesDeployment ensures the deployment of the LN features agenda
@ -181,3 +185,692 @@ func TestLNFeaturesDeployment(t *testing.T) {
testLNFeaturesDeployment(t, &chaincfg.MainNetParams, 5)
testLNFeaturesDeployment(t, &chaincfg.RegNetParams, 6)
}
// testFixSeqLocksDeployment ensures the deployment of the fix sequence locks
// agenda activates for the provided network parameters and expected deployment
// version.
func testFixSeqLocksDeployment(t *testing.T, params *chaincfg.Params, deploymentVer uint32) {
// Clone the parameters so they can be mutated, find the correct deployment
// for the fix sequence locks agenda and ensure it is always available to
// vote by removing the time constraints to prevent test failures when the
// real expiration time passes.
params = cloneParams(params)
var deployment *chaincfg.ConsensusDeployment
deployments := params.Deployments[deploymentVer]
for deploymentID, depl := range deployments {
if depl.Vote.Id == chaincfg.VoteIDFixLNSeqLocks {
deployment = &deployments[deploymentID]
break
}
}
if deployment == nil {
t.Fatalf("Unable to find consensus deployement for %s",
chaincfg.VoteIDFixLNSeqLocks)
}
deployment.StartTime = 0 // Always available for vote.
deployment.ExpireTime = math.MaxUint64 // Never expires.
// Find the correct choice for the yes vote.
const yesVoteID = "yes"
var yesChoice *chaincfg.Choice
for i, choice := range deployment.Vote.Choices {
if choice.Id == yesVoteID {
yesChoice = &deployment.Vote.Choices[i]
}
}
if yesChoice.Id != yesVoteID {
t.Fatalf("Unable to find vote choice for id %q", yesVoteID)
}
// Shorter versions of params for convenience.
stakeValidationHeight := uint32(params.StakeValidationHeight)
ruleChangeActivationInterval := params.RuleChangeActivationInterval
tests := []struct {
name string
numNodes uint32 // num fake nodes to create
curActive bool // whether agenda active for current block
nextActive bool // whether agenda active for NEXT block
}{
{
name: "stake validation height",
numNodes: stakeValidationHeight,
curActive: false,
nextActive: false,
},
{
name: "started",
numNodes: ruleChangeActivationInterval,
curActive: false,
nextActive: false,
},
{
name: "lockedin",
numNodes: ruleChangeActivationInterval,
curActive: false,
nextActive: false,
},
{
name: "one before active",
numNodes: ruleChangeActivationInterval - 1,
curActive: false,
nextActive: true,
},
{
name: "exactly active",
numNodes: 1,
curActive: true,
nextActive: true,
},
{
name: "one after active",
numNodes: 1,
curActive: true,
nextActive: true,
},
}
curTimestamp := time.Now()
bc := newFakeChain(params)
node := bc.bestChain.Tip()
for _, test := range tests {
for i := uint32(0); i < test.numNodes; i++ {
node = newFakeNode(node, int32(deploymentVer), deploymentVer, 0,
curTimestamp)
// Create fake votes that vote yes on the agenda to ensure it is
// activated.
for j := uint16(0); j < params.TicketsPerBlock; j++ {
node.votes = append(node.votes, stake.VoteVersionTuple{
Version: deploymentVer,
Bits: yesChoice.Bits | 0x01,
})
}
bc.bestChain.SetTip(node)
curTimestamp = curTimestamp.Add(time.Second)
}
// Ensure the agenda reports the expected activation status for the
// current block.
gotActive, err := bc.isFixSeqLocksAgendaActive(node.parent)
if err != nil {
t.Errorf("%s: unexpected err: %v", test.name, err)
continue
}
if gotActive != test.curActive {
t.Errorf("%s: mismatched current active status - got: %v, want: %v",
test.name, gotActive, test.curActive)
continue
}
// Ensure the agenda reports the expected activation status for the NEXT
// block
gotActive, err = bc.IsFixSeqLocksAgendaActive()
if err != nil {
t.Errorf("%s: unexpected err: %v", test.name, err)
continue
}
if gotActive != test.nextActive {
t.Errorf("%s: mismatched next active status - got: %v, want: %v",
test.name, gotActive, test.nextActive)
continue
}
}
}
// TestFixSeqLocksDeployment ensures the deployment of the fix sequence locks
// agenda activates as expected.
func TestFixSeqLocksDeployment(t *testing.T) {
testFixSeqLocksDeployment(t, &chaincfg.MainNetParams, 6)
testFixSeqLocksDeployment(t, &chaincfg.RegNetParams, 7)
}
// TestFixedSequenceLocks ensures that sequence locks within blocks behave as
// expected once the fix sequence locks agenda is active.
func TestFixedSequenceLocks(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()
// fslVersion is the deployment version of the fix sequence locks vote for
// the chain params.
const fslVersion = 7
// Find the correct deployment for the LN features agenda.
fslVoteID := chaincfg.VoteIDFixLNSeqLocks
var deployment *chaincfg.ConsensusDeployment
deployments := params.Deployments[fslVersion]
for deploymentID, depl := range deployments {
if depl.Vote.Id == fslVoteID {
deployment = &deployments[deploymentID]
break
}
}
if deployment == nil {
t.Fatalf("Unable to find consensus deployement for %s", fslVoteID)
}
// Find the correct choice for the yes vote.
const yesVoteID = "yes"
var fslYes *chaincfg.Choice
for i, choice := range deployment.Vote.Choices {
if choice.Id == yesVoteID {
fslYes = &deployment.Vote.Choices[i]
}
}
if fslYes == nil {
t.Fatalf("Unable to find vote choice for id %q", yesVoteID)
}
// Create a test generator instance initialized with the genesis block as
// the tip.
g, err := chaingen.MakeGenerator(params)
if err != nil {
t.Fatalf("Failed to create generator: %v", err)
}
// Create a new database and chain instance to run tests against.
chain, teardownFunc, err := chainSetup("seqlocksoldsemanticstest", params)
if err != nil {
t.Fatalf("Failed to setup chain instance: %v", err)
}
defer teardownFunc()
// accepted processes the current tip block associated with the generator
// and expects it to be accepted to the main chain.
//
// expectTip expects the provided block to be the current tip of the
// main chain.
//
// acceptedToSideChainWithExpectedTip expects the block to be accepted to a
// side chain, but the current best chain tip to be the provided value.
//
// testThresholdState queries the threshold state from the current tip block
// associated with the generator and expects the returned state to match the
// provided value.
accepted := func() {
msgBlock := g.Tip()
blockHeight := msgBlock.Header.Height
block := dcrutil.NewBlock(msgBlock)
t.Logf("Testing block %s (hash %s, height %d)", g.TipName(),
block.Hash(), blockHeight)
forkLen, isOrphan, err := chain.ProcessBlock(block, BFNone)
if err != nil {
t.Fatalf("block %q (hash %s, height %d) should have been "+
"accepted: %v", g.TipName(), block.Hash(), blockHeight, err)
}
// Ensure the main chain and orphan flags match the values specified in
// the test.
isMainChain := !isOrphan && forkLen == 0
if !isMainChain {
t.Fatalf("block %q (hash %s, height %d) unexpected main chain "+
"flag -- got %v, want true", g.TipName(), block.Hash(),
blockHeight, isMainChain)
}
if isOrphan {
t.Fatalf("block %q (hash %s, height %d) unexpected orphan flag -- "+
"got %v, want false", g.TipName(), block.Hash(), blockHeight,
isOrphan)
}
}
expectTip := func(tipName string) {
// Ensure hash and height match.
wantTip := g.BlockByName(tipName)
best := chain.BestSnapshot()
if best.Hash != wantTip.BlockHash() ||
best.Height != int64(wantTip.Header.Height) {
t.Fatalf("block %q (hash %s, height %d) should be the current tip "+
"-- got (hash %s, height %d)", tipName, wantTip.BlockHash(),
wantTip.Header.Height, best.Hash, best.Height)
}
}
acceptedToSideChainWithExpectedTip := func(tipName string) {
msgBlock := g.Tip()
blockHeight := msgBlock.Header.Height
block := dcrutil.NewBlock(msgBlock)
t.Logf("Testing block %s (hash %s, height %d)", g.TipName(),
block.Hash(), blockHeight)
forkLen, isOrphan, err := chain.ProcessBlock(block, BFNone)
if err != nil {
t.Fatalf("block %q (hash %s, height %d) should have been "+
"accepted: %v", g.TipName(), block.Hash(), blockHeight, err)
}
// Ensure the main chain and orphan flags match the values specified in
// the test.
isMainChain := !isOrphan && forkLen == 0
if isMainChain {
t.Fatalf("block %q (hash %s, height %d) unexpected main chain "+
"flag -- got %v, want false", g.TipName(), block.Hash(),
blockHeight, isMainChain)
}
if isOrphan {
t.Fatalf("block %q (hash %s, height %d) unexpected orphan flag -- "+
"got %v, want false", g.TipName(), block.Hash(), blockHeight,
isOrphan)
}
expectTip(tipName)
}
testThresholdState := func(id string, state ThresholdState) {
tipHash := g.Tip().BlockHash()
s, err := chain.NextThresholdState(&tipHash, fslVersion, id)
if err != nil {
t.Fatalf("block %q (hash %s, height %d) unexpected error when "+
"retrieving threshold state: %v", g.TipName(), tipHash,
g.Tip().Header.Height, err)
}
if s.State != state {
t.Fatalf("block %q (hash %s, height %d) unexpected threshold "+
"state for %s -- got %v, want %v", g.TipName(), tipHash,
g.Tip().Header.Height, id, s.State, state)
}
}
// replaceFixSeqLocksVersions is a munge function which modifies the
// provided block by replacing the block, stake, and vote versions with the
// fix sequence locks deployment version.
replaceFixSeqLocksVersions := func(b *wire.MsgBlock) {
chaingen.ReplaceBlockVersion(fslVersion)(b)
chaingen.ReplaceStakeVersion(fslVersion)(b)
chaingen.ReplaceVoteVersions(fslVersion)(b)
}
// Shorter versions of useful params for convenience.
ticketsPerBlock := int64(params.TicketsPerBlock)
coinbaseMaturity := params.CoinbaseMaturity
stakeEnabledHeight := params.StakeEnabledHeight
stakeValidationHeight := params.StakeValidationHeight
stakeVerInterval := params.StakeVersionInterval
ruleChangeInterval := int64(params.RuleChangeActivationInterval)
// ---------------------------------------------------------------------
// First block.
// ---------------------------------------------------------------------
// Add the required first block.
//
// genesis -> bp
g.CreatePremineBlock("bp", 0)
g.AssertTipHeight(1)
accepted()
// ---------------------------------------------------------------------
// Generate enough blocks to have mature coinbase outputs to work with.
//
// genesis -> bp -> bm0 -> bm1 -> ... -> bm#
// ---------------------------------------------------------------------
for i := uint16(0); i < coinbaseMaturity; i++ {
blockName := fmt.Sprintf("bm%d", i)
g.NextBlock(blockName, nil, nil)
g.SaveTipCoinbaseOuts()
accepted()
}
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)
g.SaveTipCoinbaseOuts()
accepted()
}
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.
//
// The blocks are also generated with the deployment version to ensure
// stake version and fix sequence locks enforcement is reached.
//
// ... -> bse# -> bsv0 -> bsv1 -> ... -> bsv#
// ---------------------------------------------------------------------
targetPoolSize := int64(g.Params().TicketPoolSize) * ticketsPerBlock
for i := int64(0); int64(g.Tip().Header.Height) < stakeValidationHeight; 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,
chaingen.ReplaceBlockVersion(fslVersion))
g.SaveTipCoinbaseOuts()
accepted()
}
g.AssertTipHeight(uint32(stakeValidationHeight))
// ---------------------------------------------------------------------
// Generate enough blocks to reach one block before the next two stake
// version intervals with block and vote versions for the fix sequence
// locks agenda and stake version 0.
//
// This will result in triggering enforcement of the stake version and
// that the stake version is the fix seqence locks version. The
// threshold state for deployment will move to started since the next
// block also coincides with the start of a new rule change activation
// interval for the chosen parameters.
//
// ... -> bsv# -> bvu0 -> bvu1 -> ... -> bvu#
// ---------------------------------------------------------------------
// Two stake versions intervals are needed since the first one is required
// to activate initial stake version enforcement while the second upgrades
// to the desired version which, in conjunction with the PoW upgrade via the
// block version allows the vote to start at the next rule change interval.
blocksNeeded := stakeValidationHeight + stakeVerInterval*2 - 1 -
int64(g.Tip().Header.Height)
for i := int64(0); i < blocksNeeded; i++ {
outs := g.OldestCoinbaseOuts()
blockName := fmt.Sprintf("bvu%d", i)
g.NextBlock(blockName, nil, outs[1:],
chaingen.ReplaceBlockVersion(fslVersion),
chaingen.ReplaceVoteVersions(fslVersion))
g.SaveTipCoinbaseOuts()
accepted()
}
testThresholdState(fslVoteID, ThresholdStarted)
// ---------------------------------------------------------------------
// Generate enough blocks to reach the next rule change interval with
// block, stake, and vote versions for the fix sequence locks agenda.
// Also, set the vote bits to include yes votes for the agenda.
//
// This will result in moving the threshold state for the fix sequence
// locks agenda to locked in.
//
// ... -> bvu# -> bvtli0 -> bvtli1 -> ... -> bvtli#
// ---------------------------------------------------------------------
for i := int64(0); i < ruleChangeInterval; i++ {
outs := g.OldestCoinbaseOuts()
blockName := fmt.Sprintf("bvtli%d", i)
g.NextBlock(blockName, nil, outs[1:], replaceFixSeqLocksVersions,
chaingen.ReplaceVotes(vbPrevBlockValid|fslYes.Bits, fslVersion))
g.SaveTipCoinbaseOuts()
accepted()
}
g.AssertBlockVersion(fslVersion)
g.AssertStakeVersion(fslVersion)
testThresholdState(fslVoteID, ThresholdLockedIn)
// ---------------------------------------------------------------------
// Generate enough blocks to reach the next rule change interval with
// block, stake, and vote versions for the fix sequence locks agenda.
//
// This will result in moving the threshold state for the fix sequence
// lock agenda to active thereby activating it.
//
// ... -> bvtli# -> bvta0 -> bvta1 -> ... -> bvta#
// ---------------------------------------------------------------------
for i := int64(0); i < ruleChangeInterval; i++ {
outs := g.OldestCoinbaseOuts()
blockName := fmt.Sprintf("bvta%d", i)
g.NextBlock(blockName, nil, outs[1:], replaceFixSeqLocksVersions)
g.SaveTipCoinbaseOuts()
accepted()
}
g.AssertBlockVersion(fslVersion)
g.AssertStakeVersion(fslVersion)
testThresholdState(fslVoteID, ThresholdActive)
// ---------------------------------------------------------------------
// Perform a series of sequence lock tests now that fix sequence locks
// enforcement is active.
// ---------------------------------------------------------------------
// enableSeqLocks modifies the passed transaction to enable sequence locks
// for the provided input.
enableSeqLocks := func(tx *wire.MsgTx, txInIdx int) {
tx.Version = 2
tx.TxIn[txInIdx].Sequence = 0
}
// ---------------------------------------------------------------------
// Create block that has a transaction with an input shared with a
// transaction in the stake tree and has several outputs used in
// subsequent blocks. Also, enable sequence locks for the first of
// those outputs.
//
// ... -> b0
// ---------------------------------------------------------------------
outs := g.OldestCoinbaseOuts()
b0 := g.NextBlock("b0", &outs[0], outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
// Save the current outputs of the spend tx and clear them.
tx := b.Transactions[1]
origOut := tx.TxOut[0]
origOpReturnOut := tx.TxOut[1]
tx.TxOut = tx.TxOut[:0]
// Evenly split the original output amount over multiple outputs.
const numOutputs = 6
amount := origOut.Value / numOutputs
for i := 0; i < numOutputs; i++ {
if i == numOutputs-1 {
amount = origOut.Value - amount*(numOutputs-1)
}
tx.AddTxOut(wire.NewTxOut(int64(amount), origOut.PkScript))
}
// Add the original op return back to the outputs and enable
// sequence locks for the first output.
tx.AddTxOut(origOpReturnOut)
enableSeqLocks(tx, 0)
})
g.SaveTipCoinbaseOuts()
accepted()
// ---------------------------------------------------------------------
// Create block that spends from an output created in the previous
// block.
//
// ... -> b0 -> b1a
// ---------------------------------------------------------------------
outs = g.OldestCoinbaseOuts()
g.NextBlock("b1a", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b0, 1, 0)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
enableSeqLocks(tx, 0)
b.AddTransaction(tx)
})
accepted()
// ---------------------------------------------------------------------
// Create block that involves reorganize to a sequence lock spending
// from an output created in a block prior to the parent also spent on
// on the side chain.
//
// ... -> b0 -> b1 -> b2
// \-> b1a
// ---------------------------------------------------------------------
g.SetTip("b0")
g.NextBlock("b1", nil, outs[1:], replaceFixSeqLocksVersions)
g.SaveTipCoinbaseOuts()
acceptedToSideChainWithExpectedTip("b1a")
outs = g.OldestCoinbaseOuts()
g.NextBlock("b2", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b0, 1, 0)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
enableSeqLocks(tx, 0)
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
expectTip("b2")
// ---------------------------------------------------------------------
// Create block that involves a sequence lock on a vote.
//
// ... -> b2 -> b3
// ---------------------------------------------------------------------
outs = g.OldestCoinbaseOuts()
g.NextBlock("b3", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
enableSeqLocks(b.STransactions[0], 0)
})
g.SaveTipCoinbaseOuts()
accepted()
// ---------------------------------------------------------------------
// Create block that involves a sequence lock on a ticket.
//
// ... -> b3 -> b4
// ---------------------------------------------------------------------
outs = g.OldestCoinbaseOuts()
g.NextBlock("b4", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
enableSeqLocks(b.STransactions[5], 0)
})
g.SaveTipCoinbaseOuts()
accepted()
// ---------------------------------------------------------------------
// Create two blocks such that the tip block involves a sequence lock
// spending from a different output of a transaction the parent block
// also spends from.
//
// ... -> b4 -> b5 -> b6
// ---------------------------------------------------------------------
outs = g.OldestCoinbaseOuts()
g.NextBlock("b5", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b0, 1, 1)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
outs = g.OldestCoinbaseOuts()
g.NextBlock("b6", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b0, 1, 2)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
enableSeqLocks(tx, 0)
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
// ---------------------------------------------------------------------
// Create block that involves a sequence lock spending from a regular
// tree transaction earlier in the block. This used to be rejected
// due to a consensus bug, however the fix sequence locks agenda allows
// it to be accepted as desired.
//
// ... -> b6 -> b7
// ---------------------------------------------------------------------
outs = g.OldestCoinbaseOuts()
g.NextBlock("b7", &outs[0], outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b, 1, 0)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
enableSeqLocks(tx, 0)
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
// ---------------------------------------------------------------------
// Create block that involves a sequence lock spending from a block
// prior to the parent. This used to be rejected due to a consensus
// bug, however the fix sequence locks agenda allows it to be accepted
// as desired.
//
// ... -> b6 -> b8 -> b9
// ---------------------------------------------------------------------
outs = g.OldestCoinbaseOuts()
g.NextBlock("b8", nil, outs[1:], replaceFixSeqLocksVersions)
g.SaveTipCoinbaseOuts()
accepted()
outs = g.OldestCoinbaseOuts()
g.NextBlock("b9", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b0, 1, 3)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
enableSeqLocks(tx, 0)
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
// ---------------------------------------------------------------------
// Create two blocks such that the tip block involves a sequence lock
// spending from a different output of a transaction the parent block
// also spends from when the parent block has been disapproved. This
// used to be rejected due to a consensus bug, however the fix sequence
// locks agenda allows it to be accepted as desired.
//
// ... -> b8 -> b10 -> b11
// ---------------------------------------------------------------------
const (
// vbDisapprovePrev and vbApprovePrev represent no and yes votes,
// respectively, on whether or not to approve the previous block.
vbDisapprovePrev = 0x0000
vbApprovePrev = 0x0001
)
outs = g.OldestCoinbaseOuts()
g.NextBlock("b10", nil, outs[1:], replaceFixSeqLocksVersions,
func(b *wire.MsgBlock) {
spend := chaingen.MakeSpendableOut(b0, 1, 4)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
outs = g.OldestCoinbaseOuts()
g.NextBlock("b11", nil, outs[1:], replaceFixSeqLocksVersions,
chaingen.ReplaceVotes(vbDisapprovePrev, fslVersion),
func(b *wire.MsgBlock) {
b.Header.VoteBits &^= vbApprovePrev
spend := chaingen.MakeSpendableOut(b0, 1, 5)
tx := g.CreateSpendTx(&spend, dcrutil.Amount(1))
enableSeqLocks(tx, 0)
b.AddTransaction(tx)
})
g.SaveTipCoinbaseOuts()
accepted()
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2016 The btcsuite developers
// Copyright (c) 2017-2018 The Decred developers
// Copyright (c) 2017-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -642,6 +642,55 @@ func (b *BlockChain) IsLNFeaturesAgendaActive() (bool, error) {
return isActive, err
}
// isFixSeqLocksAgendaActive returns whether or not the fix sequence locks
// agenda vote, as defined in DCP0004 has passed and is now active from the
// point of view of the passed block node.
//
// It is important to note that, as the variable name indicates, this function
// expects the block node prior to the block for which the deployment state is
// desired. In other words, the returned deployment state is for the block
// AFTER the passed node.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) isFixSeqLocksAgendaActive(prevNode *blockNode) (bool, error) {
// Consensus voting on the fix sequence locks agenda is only enabled on
// mainnet, testnet v3, and regnet.
net := b.chainParams.Net
if net != wire.MainNet && net != wire.TestNet3 && net != wire.RegNet {
return true, nil
}
// Determine the version for the fix sequence locks agenda as defined in
// DCP0004 for the provided network.
deploymentVer := uint32(6)
if b.chainParams.Net != wire.MainNet {
deploymentVer = 7
}
state, err := b.deploymentState(prevNode, deploymentVer,
chaincfg.VoteIDFixLNSeqLocks)
if err != nil {
return false, err
}
// NOTE: The choice field of the return threshold state is not examined
// here because there is only one possible choice that can be active for
// the agenda, which is yes, so there is no need to check it.
return state.State == ThresholdActive, nil
}
// IsFixSeqLocksAgendaActive returns whether or not whether or not the fix
// sequence locks agenda vote, as defined in DCP0004 has passed and is now
// active for the block AFTER the current best chain block.
//
// This function is safe for concurrent access.
func (b *BlockChain) IsFixSeqLocksAgendaActive() (bool, error) {
b.chainLock.Lock()
isActive, err := b.isFixSeqLocksAgendaActive(b.bestChain.Tip())
b.chainLock.Unlock()
return isActive, err
}
// VoteCounts is a compacted struct that is used to message vote counts.
type VoteCounts struct {
Total uint32

View File

@ -1,5 +1,5 @@
// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2015-2018 The Decred developers
// Copyright (c) 2015-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -983,10 +983,20 @@ func (b *BlockChain) checkBlockHeaderPositional(header *wire.BlockHeader, prevNo
}
if !fastAdd {
// Reject version 5 blocks for networks other than the main
// Reject version 6 blocks for networks other than the main
// network once a majority of the network has upgraded.
if b.chainParams.Net != wire.MainNet && header.Version < 6 &&
b.isMajorityVersion(6, prevNode, b.chainParams.BlockRejectNumRequired) {
if b.chainParams.Net != wire.MainNet && header.Version < 7 &&
b.isMajorityVersion(7, prevNode, b.chainParams.BlockRejectNumRequired) {
str := "new blocks with version %d are no longer valid"
str = fmt.Sprintf(str, header.Version)
return ruleError(ErrBlockVersionTooOld, str)
}
// Reject version 5 blocks once a majority of the network has
// upgraded.
if header.Version < 6 && b.isMajorityVersion(6, prevNode,
b.chainParams.BlockRejectNumRequired) {
str := "new blocks with version %d are no longer valid"
str = fmt.Sprintf(str, header.Version)
@ -2891,12 +2901,16 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B
// Create a view which preserves the expected consensus semantics for
// relative lock times via sequence numbers once the stake vote for the
// agenda is active.
var legacySeqLockView *UtxoViewpoint
legacySeqLockView := view
lnFeaturesActive, err := b.isLNFeaturesAgendaActive(node.parent)
if err != nil {
return err
}
if lnFeaturesActive {
fixSeqLocksActive, err := b.isFixSeqLocksAgendaActive(node.parent)
if err != nil {
return err
}
if lnFeaturesActive && !fixSeqLocksActive {
var err error
legacySeqLockView, err = b.createLegacySeqLockView(block, parent,
view)

View File

@ -1,5 +1,5 @@
// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2015-2017 The Decred developers
// Copyright (c) 2015-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -186,6 +186,13 @@ type Policy struct {
//
// This function must be safe for concurrent access.
StandardVerifyFlags func() (txscript.ScriptFlags, error)
// AcceptSequenceLocks defines the function to determine whether or not
// to accept transactions with sequence locks. Typically this will be
// set depending on the result of the fix sequence locks agenda vote.
//
// This function must be safe for concurrent access.
AcceptSequenceLocks func() (bool, error)
}
// TxDesc is a descriptor containing a transaction in the mempool along with
@ -858,19 +865,30 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
} else {
tx.SetTree(wire.TxTreeStake)
}
// Don't accept transactions with sequence locks enabled until a vote
// takes place to change the semantics.
isVote := txType == stake.TxTypeSSGen
if msgTx.Version >= 2 && !isVote {
for _, txIn := range msgTx.TxIn {
sequenceNum := txIn.Sequence
if sequenceNum&wire.SequenceLockTimeDisabled != 0 {
continue
}
str := "violates sequence lock consensus bug"
return nil, txRuleError(wire.RejectInvalid, str)
// Choose whether or not to accept transactions with sequence locks enabled.
//
// Typically, this will be set based on the result of the fix sequence locks
// agenda vote.
acceptSeqLocks, err := mp.cfg.Policy.AcceptSequenceLocks()
if err != nil {
if cerr, ok := err.(blockchain.RuleError); ok {
return nil, chainRuleError(cerr)
}
return nil, err
}
if !acceptSeqLocks {
if msgTx.Version >= 2 && !isVote {
for _, txIn := range msgTx.TxIn {
sequenceNum := txIn.Sequence
if sequenceNum&wire.SequenceLockTimeDisabled != 0 {
continue
}
str := "violates sequence lock consensus bug"
return nil, txRuleError(wire.RejectInvalid, str)
}
}
}

View File

@ -40,14 +40,15 @@ const (
// transactions to be appear as though they are spending completely valid utxos.
type fakeChain struct {
sync.RWMutex
nextStakeDiff int64
utxos *blockchain.UtxoViewpoint
utxoTimes map[wire.OutPoint]int64
blocks map[chainhash.Hash]*dcrutil.Block
currentHash chainhash.Hash
currentHeight int64
medianTime time.Time
scriptFlags txscript.ScriptFlags
nextStakeDiff int64
utxos *blockchain.UtxoViewpoint
utxoTimes map[wire.OutPoint]int64
blocks map[chainhash.Hash]*dcrutil.Block
currentHash chainhash.Hash
currentHeight int64
medianTime time.Time
scriptFlags txscript.ScriptFlags
acceptSeqLocks bool
}
// NextStakeDifficulty returns the next stake difficulty associated with the
@ -282,6 +283,23 @@ func (s *fakeChain) AddFakeUtxoMedianTime(tx *dcrutil.Tx, txOutIdx uint32, media
s.Unlock()
}
// AcceptSequenceLocks returns whether or not the pool harness the fake chain
// is associated with should accept transactions with sequence locks enabled.
func (s *fakeChain) AcceptSequenceLocks() (bool, error) {
s.RLock()
acceptSeqLocks := s.acceptSeqLocks
s.RUnlock()
return acceptSeqLocks, nil
}
// SetAcceptSequenceLocks sets whether or not the pool harness the fake chain is
// associated with should accept transactions with sequence locks enabled.
func (s *fakeChain) SetAcceptSequenceLocks(accept bool) {
s.Lock()
s.acceptSeqLocks = accept
s.Unlock()
}
// spendableOutput is a convenience type that houses a particular utxo and the
// amount associated with it.
type spendableOutput struct {
@ -708,6 +726,7 @@ func newPoolHarness(chainParams *chaincfg.Params) (*poolHarness, []spendableOutp
MaxSigOpsPerTx: blockchain.MaxSigOpsPerBlock / 5,
MinRelayTxFee: 1000, // 1 Satoshi per byte
StandardVerifyFlags: chain.StandardVerifyFlags,
AcceptSequenceLocks: chain.AcceptSequenceLocks,
},
ChainParams: chainParams,
NextStakeDifficulty: chain.NextStakeDifficulty,
@ -1468,3 +1487,303 @@ func TestMultiInputOrphanDoubleSpend(t *testing.T) {
// was not moved to the transaction pool.
testPoolMembership(tc, doubleSpendTx, false, false)
}
// mustLockTimeToSeq converts the passed relative lock time to a sequence number
// by using LockTimeToSequence. It only differs in that it will panic if there
// is an error so errors in the source code can be detected. It will only (and
// must only) be called with hard-coded, and therefore known good, values.
func mustLockTimeToSeq(isSeconds bool, lockTime uint32) uint32 {
sequence, err := blockchain.LockTimeToSequence(isSeconds, lockTime)
if err != nil {
panic(fmt.Sprintf("invalid lock time in source file: "+
"isSeconds: %v, lockTime: %d", isSeconds, lockTime))
}
return sequence
}
// seqIntervalToSecs converts the passed number of sequence lock intervals into
// the number of seconds it represents.
func seqIntervalToSecs(intervals uint32) uint32 {
return intervals << wire.SequenceLockTimeGranularity
}
// TestSequenceLockAcceptance ensures that transactions which involve sequence
// locks are accepted or rejected from the memory as expected.
func TestSequenceLockAcceptance(t *testing.T) {
t.Parallel()
// Shorter versions of variables for convenience.
const seqLockTimeDisabled = wire.SequenceLockTimeDisabled
const seqLockTimeIsSecs = wire.SequenceLockTimeIsSeconds
tests := []struct {
name string // test description.
txVersion uint16 // transaction version.
sequence uint32 // sequence number used for input.
heightOffset int64 // mock chain height offset at which to evaluate.
secsOffset int64 // mock median time offset at which to evaluate.
valid bool // whether tx is valid when enforcing seq locks.
}{
{
name: "By-height lock with seq == height == 0",
txVersion: 2,
sequence: mustLockTimeToSeq(false, 0),
heightOffset: 0,
valid: true,
},
{
// The mempool is for transactions to be included in the next block
// so sequence locks are calculated based on that point of view.
// Thus, a sequence lock of one for an input created at the current
// height will be satisified.
name: "By-height lock with seq == 1, height == 0",
txVersion: 2,
sequence: mustLockTimeToSeq(false, 1),
heightOffset: 0,
valid: true,
},
{
name: "By-height lock with seq == height == 65535",
txVersion: 2,
sequence: mustLockTimeToSeq(false, 65535),
heightOffset: 65534,
valid: true,
},
{
name: "By-height lock with masked max seq == height",
txVersion: 2,
sequence: 0xffffffff &^ seqLockTimeDisabled &^ seqLockTimeIsSecs,
heightOffset: 65534,
valid: true,
},
{
name: "By-height lock with unsatisfied seq == 2",
txVersion: 2,
sequence: mustLockTimeToSeq(false, 2),
heightOffset: 0,
valid: false,
},
{
name: "By-height lock with unsatisfied masked max sequence",
txVersion: 2,
sequence: 0xffffffff &^ seqLockTimeDisabled &^ seqLockTimeIsSecs,
heightOffset: 65533,
valid: false,
},
{
name: "By-time lock with seq == elapsed == 0",
txVersion: 2,
sequence: mustLockTimeToSeq(true, 0),
secsOffset: 0,
valid: true,
},
{
name: "By-time lock with seq == elapsed == max",
txVersion: 2,
sequence: mustLockTimeToSeq(true, seqIntervalToSecs(65535)),
secsOffset: int64(seqIntervalToSecs(65535)),
valid: true,
},
{
name: "By-time lock with unsatisifed seq == 1024",
txVersion: 2,
sequence: mustLockTimeToSeq(true, seqIntervalToSecs(2)),
secsOffset: int64(seqIntervalToSecs(1)),
valid: false,
},
{
name: "By-time lock with unsatisifed masked max sequence",
txVersion: 2,
sequence: 0xffffffff &^ seqLockTimeDisabled,
secsOffset: int64(seqIntervalToSecs(65534)),
valid: false,
},
{
name: "Disabled by-height lock with seq == height == 0",
txVersion: 2,
sequence: mustLockTimeToSeq(false, 0) | seqLockTimeDisabled,
heightOffset: 0,
valid: true,
},
{
name: "Disabled by-height lock with unsatisified sequence",
txVersion: 2,
sequence: mustLockTimeToSeq(false, 2) | seqLockTimeDisabled,
heightOffset: 0,
valid: true,
},
{
name: "Disabled by-time lock with seq == elapsed == 0",
txVersion: 2,
sequence: mustLockTimeToSeq(true, 0) | seqLockTimeDisabled,
secsOffset: 0,
valid: true,
},
{
name: "Disabled by-time lock with unsatisifed seq == 1024",
txVersion: 2,
sequence: mustLockTimeToSeq(true, seqIntervalToSecs(2)) |
seqLockTimeDisabled,
secsOffset: int64(seqIntervalToSecs(1)),
valid: true,
},
// The following section uses version 1 transactions which are not
// subject to sequence locks.
{
name: "By-height lock with seq == height == 0 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(false, 0),
heightOffset: 0,
valid: true,
},
{
name: "By-height lock with unsatisfied seq == 2 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(false, 2),
heightOffset: 0,
valid: true,
},
{
name: "By-time lock with seq == elapsed == 0 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(true, 0),
secsOffset: 0,
valid: true,
},
{
name: "By-time lock with unsatisifed seq == 1024 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(true, seqIntervalToSecs(2)),
secsOffset: int64(seqIntervalToSecs(1)),
valid: true,
},
{
name: "Disabled by-height lock with seq == height == 0 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(false, 0) | seqLockTimeDisabled,
heightOffset: 0,
valid: true,
},
{
name: "Disabled by-height lock with unsatisified seq (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(false, 2) | seqLockTimeDisabled,
heightOffset: 0,
valid: true,
},
{
name: "Disabled by-time lock with seq == elapsed == 0 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(true, 0) | seqLockTimeDisabled,
secsOffset: 0,
valid: true,
},
{
name: "Disabled by-time lock with unsatisifed seq == 1024 (v1)",
txVersion: 1,
sequence: mustLockTimeToSeq(true, seqIntervalToSecs(2)) |
seqLockTimeDisabled,
secsOffset: int64(seqIntervalToSecs(1)),
valid: true,
},
}
// Run through the tests twice such that the first time the pool is set to
// reject all sequence locks and the second it is not.
for _, acceptSeqLocks := range []bool{false, true} {
harness, _, err := newPoolHarness(&chaincfg.MainNetParams)
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
tc := &testContext{t, harness}
harness.chain.SetAcceptSequenceLocks(acceptSeqLocks)
baseHeight := harness.chain.BestHeight()
baseTime := time.Now()
for i, test := range tests {
// Create and add a mock utxo at a common base height so updating
// the mock chain height below will cause sequence locks to be
// evaluated relative to that height.
//
// The output value adds the test index in order to ensure the
// resulting transaction hash is unique.
inputMsgTx := wire.NewMsgTx()
inputMsgTx.AddTxOut(&wire.TxOut{
PkScript: harness.payScript,
Value: 1000000000 + int64(i),
})
inputTx := dcrutil.NewTx(inputMsgTx)
harness.AddFakeUTXO(inputTx, baseHeight)
harness.chain.AddFakeUtxoMedianTime(inputTx, 0, baseTime)
// Create a transaction which spends from the mock utxo with the
// details specified in the test data.
spendableOut := txOutToSpendableOut(inputTx, 0, wire.TxTreeRegular)
inputs := []spendableOutput{spendableOut}
tx, err := harness.CreateSignedTx(inputs, 1, func(tx *wire.MsgTx) {
tx.Version = test.txVersion
tx.TxIn[0].Sequence = test.sequence
})
if err != nil {
t.Fatalf("unable to create tx: %v", err)
}
// Determine if the test data describes a transaction with an
// enabled sequence lock.
hasEnabledSeqLock := test.txVersion >= 2 &&
test.sequence&wire.SequenceLockTimeDisabled == 0
// Set the mock chain height and median time based on the test data
// and ensure the transaction is either accepted or rejected as
// desired.
secsOffset := time.Second * time.Duration(test.secsOffset)
harness.chain.SetHeight(baseHeight + test.heightOffset)
harness.chain.SetPastMedianTime(baseTime.Add(secsOffset))
acceptedTxns, err := harness.txPool.ProcessTransaction(tx, false,
false, true)
switch {
case !acceptSeqLocks && hasEnabledSeqLock && err == nil:
t.Fatalf("%s: did not reject tx when seq locks are not allowed",
test.name)
case !acceptSeqLocks && !hasEnabledSeqLock && err != nil:
t.Fatalf("%s: did not accept tx: %v", test.name, err)
case acceptSeqLocks && test.valid && err != nil:
t.Fatalf("%s: did not accept tx: %v", test.name, err)
case acceptSeqLocks && !test.valid && err == nil:
t.Fatalf("%s: did not reject tx", test.name)
}
// Ensure the number of reported accepted transactions and pool
// membership matches the expected result.
shouldHaveAccepted := (acceptSeqLocks && test.valid) ||
(!acceptSeqLocks && !hasEnabledSeqLock)
switch {
case shouldHaveAccepted:
// Ensure the transaction was reported as accepted.
if len(acceptedTxns) != 1 {
t.Fatalf("%s: reported %d accepted transactions from what "+
"should be 1", test.name, len(acceptedTxns))
}
// Ensure the transaction is not in the orphan pool, in the
// transaction pool, and reported as available.
testPoolMembership(tc, tx, false, true)
case !shouldHaveAccepted:
if len(acceptedTxns) != 0 {
// Ensure no transactions were reported as accepted.
t.Fatalf("%s: reported %d accepted transactions from what "+
"should have been rejected", test.name, len(acceptedTxns))
}
// Ensure the transaction is not in the orphan pool, not in the
// transaction pool, and not reported as available.
testPoolMembership(tc, tx, false, false)
}
}
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2014-2016 The btcsuite developers
// Copyright (c) 2015-2018 The Decred developers
// Copyright (c) 2015-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -30,11 +30,11 @@ const (
// will require changes to the generated block. Using the wire constant
// for generated block version could allow creation of invalid blocks
// for the updated version.
generatedBlockVersion = 5
generatedBlockVersion = 6
// generatedBlockVersionTest is the version of the block being generated
// for networks other than the main and simulation networks.
generatedBlockVersionTest = 6
generatedBlockVersionTest = 7
// blockHeaderOverhead is the max number of bytes it takes to serialize
// a block header and max possible transaction count.

View File

@ -1,5 +1,5 @@
// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2015-2018 The Decred developers
// Copyright (c) 2015-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -2554,6 +2554,7 @@ func newServer(listenAddrs []string, db database.DB, chainParams *chaincfg.Param
StandardVerifyFlags: func() (txscript.ScriptFlags, error) {
return standardScriptVerifyFlags(bm.chain)
},
AcceptSequenceLocks: bm.chain.IsFixSeqLocksAgendaActive,
},
ChainParams: chainParams,
NextStakeDifficulty: func() (int64, error) {