From 5697f846e7e26cc68059a3805f29dd37e0024bb1 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 23 Aug 2021 09:21:08 -0500 Subject: [PATCH] blockchain: Implement reject new script vers vote. This implements the agenda for voting on rejecting new script versions until they have explicitly been enabled by a consensus rule change as defined in DCP0008 along with consensus tests. In particular, once the vote has passed and is active, script versions greater than 0 will no longer be accepted. Note that this does not implement block version bumps in the mining and validation code that will ultimately be needed since another consensus vote is expected to land before voting begins. The following is an overview of the changes: - Modify block validation to enforce the rejection of newer script versions in accordance with the state of the vote - Add tests to ensure proper behavior for new script versions as follows: - Ensure stake transactions still require version 0 scripts regardless of the state of the agenda - Regular transactions with script versions newer than 0 are rejected once the agenda is active - Transaction outputs that have a non-zero output and script that would make the output unspendable if it were version 0 are spendable with a nil signature script both before the agenda is active and remain spendable after it is active --- blockchain/error.go | 4 ++ blockchain/error_test.go | 1 + blockchain/validate.go | 30 +++++++++++- blockchain/validate_test.go | 94 +++++++++++++++++++++++++++++++++++-- 4 files changed, 123 insertions(+), 6 deletions(-) diff --git a/blockchain/error.go b/blockchain/error.go index 26f06fd057..724c6b2bd0 100644 --- a/blockchain/error.go +++ b/blockchain/error.go @@ -152,6 +152,10 @@ const ( // range or not referencing one at all. ErrBadTxInput = ErrorKind("ErrBadTxInput") + // ErrScriptVersionTooHigh indicates a transaction script version is higher + // than the maximum version allowed by the active consensus rules. + ErrScriptVersionTooHigh = ErrorKind("ErrScriptVersionTooHigh") + // ErrMissingTxOut indicates a transaction output referenced by an input // either does not exist or has already been spent. ErrMissingTxOut = ErrorKind("ErrMissingTxOut") diff --git a/blockchain/error_test.go b/blockchain/error_test.go index 824746dde0..c98bfa22ab 100644 --- a/blockchain/error_test.go +++ b/blockchain/error_test.go @@ -45,6 +45,7 @@ func TestErrorKindStringer(t *testing.T) { {ErrDuplicateTxInputs, "ErrDuplicateTxInputs"}, {ErrTxVersionTooHigh, "ErrTxVersionTooHigh"}, {ErrBadTxInput, "ErrBadTxInput"}, + {ErrScriptVersionTooHigh, "ErrScriptVersionTooHigh"}, {ErrMissingTxOut, "ErrMissingTxOut"}, {ErrUnfinalizedTx, "ErrUnfinalizedTx"}, {ErrDuplicateTx, "ErrDuplicateTx"}, diff --git a/blockchain/validate.go b/blockchain/validate.go index c1f601500f..bbb2614bb8 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -514,12 +514,38 @@ func checkTransactionContext(tx *wire.MsgTx, params *chaincfg.Params, flags Agen } } - // Ensure that non-stake transactions have no outputs with opcodes that are - // not allowed outside of the stake transactions. + // Enforce additional rules on regular (non-stake) transactions. isStakeTx := isVote || isTicket || isRevocation || isTreasuryAdd || isTreasurySpend || isTreasuryBase if !isStakeTx { + // Note that prior to the explicit version upgrades agenda, transaction + // script versions are allowed to go up to a max uint16, so fall back to + // that value accordingly. + maxAllowedScriptVer := ^uint16(0) + switch { + case explicitUpgradesActive: + maxAllowedScriptVer = 0 + } + for txOutIdx, txOut := range tx.TxOut { + // Reject transaction script versions greater than the highest + // currently supported version. Any future consensus changes that + // result in introduction of a new script version are expected to + // update this code accordingly so that the newer transaction script + // version can be used as a guaranteed proxy for an agenda having + // passed and become active. + // + // It is also worth noting that this check only applies to regular + // transactions because stake transactions are individually and + // separately enforced to be a specific script version. + if txOut.Version > maxAllowedScriptVer { + str := fmt.Sprintf("script version %d is greater than the max "+ + "allowed version %d)", txOut.Version, maxAllowedScriptVer) + return ruleError(ErrScriptVersionTooHigh, str) + } + + // Ensure that non-stake transactions have no outputs with opcodes + // that are not allowed outside of the stake transactions. hasOp, err := txscript.ContainsStakeOpCodes(txOut.PkScript, isTreasuryEnabled) if err != nil { diff --git a/blockchain/validate_test.go b/blockchain/validate_test.go index 0cf89d449e..8502147dc8 100644 --- a/blockchain/validate_test.go +++ b/blockchain/validate_test.go @@ -23,6 +23,7 @@ import ( "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/database/v3" "github.com/decred/dcrd/dcrutil/v4" + "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/wire" ) @@ -1080,8 +1081,12 @@ func TestExplicitVerUpgradesSemantics(t *testing.T) { // ------------------------------------------------------------------------- // Create block at stake validation height that has both a regular and stake // transaction with a version allowed prior to the explicit version upgrades - // agenda but not after it activates. The outputs are created now so they - // can be spent after the agenda activates to ensure they remain spendable. + // agenda but not after it activates. Also, set one of the outputs of + // another regular transaction to a script version that is also allowed + // prior to the explicit version upgrades agenda but not after it activates. + // + // The outputs are created now so they can be spent after the agenda + // activates to ensure they remain spendable. // // The block should be accepted because the agenda is not active yet. // @@ -1103,14 +1108,41 @@ func TestExplicitVerUpgradesSemantics(t *testing.T) { } // Set transaction versions to values that will no longer be valid for - // new outputs after the explicit version upgrades agenda activates. + // new transactions after the explicit version upgrades agenda + // activates. b.Transactions[2].Version = ^uint16(0) b.STransactions[3].Version = ^uint16(0) + + // Set output script version of a regular transaction output to a value + // that will no longer be valid for new outputs after the explicit + // version upgrades agenda activates. Also, set the script to false + // which ordinarily would make the output unspendable if the script + // version were 0. + b.Transactions[3].TxOut[0].Version = 1 + b.Transactions[3].TxOut[0].PkScript = []byte{txscript.OP_FALSE} }) g.SaveTipCoinbaseOuts() g.AcceptTipBlock() g.SnapshotCoinbaseOuts("postSVH") + // ------------------------------------------------------------------------- + // Create block that has a stake transaction with a script version that is + // not allowed regardless of the explicit version upgrades agenda. + // + // The block should be rejected because stake transaction script versions + // are separately enforced and are required to be a specific version + // independently of the consensus change. + // + // ... -> bsvh + // \-> bbadvote + // ------------------------------------------------------------------------- + + outs = g.OldestCoinbaseOuts() + g.NextBlock("bbadvote", &outs[0], outs[1:], func(b *wire.MsgBlock) { + b.STransactions[4].TxOut[2].Version = 12345 + }) + g.RejectTipBlock(ErrBadTxInput) + // ------------------------------------------------------------------------- // Create enough blocks to allow the stake output created at stake // validation height to mature. @@ -1118,7 +1150,7 @@ func TestExplicitVerUpgradesSemantics(t *testing.T) { // ... -> bsvh -> btmp0 -> ... -> btmp# // ------------------------------------------------------------------------- - outs = g.OldestCoinbaseOuts() + g.SetTip("bsvh") for i := uint16(0); i < coinbaseMaturity; i++ { blockName := fmt.Sprintf("btmp%d", i) g.NextBlock(blockName, &outs[0], outs[1:]) @@ -1154,6 +1186,15 @@ func TestExplicitVerUpgradesSemantics(t *testing.T) { stakeSpend := chaingen.MakeSpendableStakeOut(bsvh, stakeSpendTxIdx, 2) stakeSpendTx := g.CreateSpendTx(&stakeSpend, lowFee) b.AddTransaction(stakeSpendTx) + + // Notice the signature script is nil because the version is unsupported + // which means the scripts are never executed. + const secondRegSpendTxIdx = 3 + regSpend = chaingen.MakeSpendableOut(bsvh, secondRegSpendTxIdx, 0) + regSpendTx = g.CreateSpendTx(®Spend, lowFee) + regSpendTx.TxIn[0].SignatureScript = nil + b.AddTransaction(regSpendTx) + }) g.AcceptTipBlock() @@ -1237,6 +1278,23 @@ func TestExplicitVerUpgradesSemantics(t *testing.T) { }) g.RejectTipBlock(ErrTxVersionTooHigh) + // ------------------------------------------------------------------------- + // Create block that has a regular transaction with an output that has a + // script version that is no longer allowed for new transactions after the + // explicit version upgrades agenda is active. + // + // The block should be rejected because the agenda is active. + // + // ... -> bbase + // \-> b0bad3 + // ------------------------------------------------------------------------- + + g.SetTip("bbase") + g.NextBlock("b0bad3", &outs[0], outs[1:], replaceVers, func(b *wire.MsgBlock) { + b.Transactions[1].TxOut[0].Version = ^uint16(0) + }) + g.RejectTipBlock(ErrScriptVersionTooHigh) + // ------------------------------------------------------------------------- // Create block that spends both the regular and stake transactions created // with a version that is no longer allowed for new transactions after the @@ -1266,4 +1324,32 @@ func TestExplicitVerUpgradesSemantics(t *testing.T) { }) g.SaveTipCoinbaseOuts() g.AcceptTipBlock() + + // ------------------------------------------------------------------------- + // Create block that spends the regular transaction output created with a + // script version that is no longer allowed for new outputs after the + // explicit version upgrades agenda is active but already existed prior to + // its activation. + // + // The block should be accepted because all existing utxos must remain + // spendable after the agenda activates. + // + // ... -> bbase -> b0 -> b1 + // ------------------------------------------------------------------------- + + outs = g.OldestCoinbaseOuts() + g.NextBlock("b1", &outs[0], outs[1:], replaceVers, func(b *wire.MsgBlock) { + // Notice the output is still spendable even though the signature script + // is nil because the version is unsupported which means the scripts are + // never executed. + const regSpendTxIdx = 3 + bsvh := g.BlockByName("bsvh") + regSpend := chaingen.MakeSpendableOut(bsvh, regSpendTxIdx, 0) + regSpendTx := g.CreateSpendTx(®Spend, lowFee) + regSpendTx.TxIn[0].SignatureScript = nil + b.AddTransaction(regSpendTx) + }) + g.SaveTipCoinbaseOuts() + g.AcceptTipBlock() + }