diff --git a/blockchain/agendas_test.go b/blockchain/agendas_test.go index e1d9de99..f5681807 100644 --- a/blockchain/agendas_test.go +++ b/blockchain/agendas_test.go @@ -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() +} diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index a6d19fd7..88e31c88 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -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 diff --git a/blockchain/validate.go b/blockchain/validate.go index 93c5ad8a..306f958e 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -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) diff --git a/mempool/mempool.go b/mempool/mempool.go index 471296b8..50ef7f9c 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -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) + } } } diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index 92aa39e9..43030070 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -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) + } + } + } +} diff --git a/mining.go b/mining.go index ff24b485..2338e6f4 100644 --- a/mining.go +++ b/mining.go @@ -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. diff --git a/server.go b/server.go index e3c34bbe..c877c46d 100644 --- a/server.go +++ b/server.go @@ -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) {