// Copyright (c) 2016 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. // This file is ignored during the regular tests due to the following build tag. //go:build rpctest // +build rpctest package integration import ( "bytes" "runtime" "strings" "testing" "time" "github.com/lbryio/lbcd/blockchain" "github.com/lbryio/lbcd/btcec" "github.com/lbryio/lbcd/chaincfg" "github.com/lbryio/lbcd/chaincfg/chainhash" "github.com/lbryio/lbcd/integration/rpctest" "github.com/lbryio/lbcd/txscript" "github.com/lbryio/lbcd/wire" btcutil "github.com/lbryio/lbcutil" ) const ( csvKey = "csv" ) // makeTestOutput creates an on-chain output paying to a freshly generated // p2pkh output with the specified amount. func makeTestOutput(r *rpctest.Harness, t *testing.T, amt btcutil.Amount) (*btcec.PrivateKey, *wire.OutPoint, []byte, error) { // Create a fresh key, then send some coins to an address spendable by // that key. key, err := btcec.NewPrivateKey(btcec.S256()) if err != nil { return nil, nil, nil, err } // Using the key created above, generate a pkScript which it's able to // spend. a, err := btcutil.NewAddressPubKey(key.PubKey().SerializeCompressed(), r.ActiveNet) if err != nil { return nil, nil, nil, err } selfAddrScript, err := txscript.PayToAddrScript(a.AddressPubKeyHash()) if err != nil { return nil, nil, nil, err } output := &wire.TxOut{PkScript: selfAddrScript, Value: 1e8} // Next, create and broadcast a transaction paying to the output. fundTx, err := r.CreateTransaction([]*wire.TxOut{output}, 10, true) if err != nil { return nil, nil, nil, err } txHash, err := r.Client.SendRawTransaction(fundTx, true) if err != nil { return nil, nil, nil, err } // The transaction created above should be included within the next // generated block. blockHash, err := r.Client.Generate(1) if err != nil { return nil, nil, nil, err } assertTxInBlock(r, t, blockHash[0], txHash) // Locate the output index of the coins spendable by the key we // generated above, this is needed in order to create a proper utxo for // this output. var outputIndex uint32 if bytes.Equal(fundTx.TxOut[0].PkScript, selfAddrScript) { outputIndex = 0 } else { outputIndex = 1 } utxo := &wire.OutPoint{ Hash: fundTx.TxHash(), Index: outputIndex, } return key, utxo, selfAddrScript, nil } // TestBIP0113Activation tests for proper adherence of the BIP 113 rule // constraint which requires all transaction finality tests to use the MTP of // the last 11 blocks, rather than the timestamp of the block which includes // them. // // Overview: // // - Pre soft-fork: // // - Transactions with non-final lock-times from the PoV of MTP should be // rejected from the mempool. // // - Transactions within non-final MTP based lock-times should be accepted // in valid blocks. // // - Post soft-fork: // // - Transactions with non-final lock-times from the PoV of MTP should be // rejected from the mempool and when found within otherwise valid blocks. // // - Transactions with final lock-times from the PoV of MTP should be // accepted to the mempool and mined in future block. func TestBIP0113Activation(t *testing.T) { t.Parallel() btcdCfg := []string{"--rejectnonstd"} r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") if err != nil { t.Fatal("unable to create primary harness: ", err) } if err := r.SetUp(true, 10); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() // Create a fresh output for usage within the test below. const outputValue = btcutil.SatoshiPerBitcoin / 50 outputKey, testOutput, testPkScript, err := makeTestOutput(r, t, outputValue) if err != nil { t.Fatalf("unable to create test output: %v", err) } // Fetch a fresh address from the harness, we'll use this address to // send funds back into the Harness. addr, err := r.NewAddress() if err != nil { t.Fatalf("unable to generate address: %v", err) } addrScript, err := txscript.PayToAddrScript(addr) if err != nil { t.Fatalf("unable to generate addr script: %v", err) } // Now create a transaction with a lock time which is "final" according // to the latest block, but not according to the current median time // past. tx := wire.NewMsgTx(1) tx.AddTxIn(&wire.TxIn{ PreviousOutPoint: *testOutput, }) tx.AddTxOut(&wire.TxOut{ PkScript: addrScript, Value: outputValue - 1000, }) // We set the lock-time of the transaction to just one minute after the // current MTP of the chain. chainInfo, err := r.Client.GetBlockChainInfo() if err != nil { t.Fatalf("unable to query for chain info: %v", err) } tx.LockTime = uint32(chainInfo.MedianTime) + 1 sigScript, err := txscript.SignatureScript(tx, 0, testPkScript, txscript.SigHashAll, outputKey, true) if err != nil { t.Fatalf("unable to generate sig: %v", err) } tx.TxIn[0].SignatureScript = sigScript // This transaction should be rejected from the mempool as using MTP // for transactions finality is now a policy rule. Additionally, the // exact error should be the rejection of a non-final transaction. _, err = r.Client.SendRawTransaction(tx, true) if err == nil { t.Fatalf("transaction accepted, but should be non-final") } else if !strings.Contains(err.Error(), "not finalized") { t.Fatalf("transaction should be rejected due to being "+ "non-final, instead: %v", err) } // However, since the block validation consensus rules haven't yet // activated, a block including the transaction should be accepted. txns := []*btcutil.Tx{btcutil.NewTx(tx)} block, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{}) if err != nil { t.Fatalf("unable to submit block: %v", err) } txid := tx.TxHash() assertTxInBlock(r, t, block.Hash(), &txid) // At this point, the block height should be 103: we mined 101 blocks // to create a single mature output, then an additional block to create // a new output, and then mined a single block above to include our // transaction. assertChainHeight(r, t, 112) // Next, mine enough blocks to ensure that the soft-fork becomes // activated. Assert that the block version of the second-to-last block // in the final range is active. // Next, mine ensure blocks to ensure that the soft-fork becomes // active. We're at height 103 and we need 200 blocks to be mined after // the genesis target period, so we mine 196 blocks. This'll put us at // height 299. The getblockchaininfo call checks the state for the // block AFTER the current height. numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 4 if _, err := r.Client.Generate(numBlocks); err != nil { t.Fatalf("unable to generate blocks: %v", err) } assertChainHeight(r, t, 308) assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdActive) // The timeLockDeltas slice represents a series of deviations from the // current MTP which will be used to test border conditions w.r.t // transaction finality. -1 indicates 1 second prior to the MTP, 0 // indicates the current MTP, and 1 indicates 1 second after the // current MTP. // // This time, all transactions which are final according to the MTP // *should* be accepted to both the mempool and within a valid block. // While transactions with lock-times *after* the current MTP should be // rejected. timeLockDeltas := []int64{-1, 0, 1} for _, timeLockDelta := range timeLockDeltas { chainInfo, err = r.Client.GetBlockChainInfo() if err != nil { t.Fatalf("unable to query for chain info: %v", err) } medianTimePast := chainInfo.MedianTime // Create another test output to be spent shortly below. outputKey, testOutput, testPkScript, err = makeTestOutput(r, t, outputValue) if err != nil { t.Fatalf("unable to create test output: %v", err) } // Create a new transaction with a lock-time past the current known // MTP. tx = wire.NewMsgTx(1) tx.AddTxIn(&wire.TxIn{ PreviousOutPoint: *testOutput, }) tx.AddTxOut(&wire.TxOut{ PkScript: addrScript, Value: outputValue - 1000, }) tx.LockTime = uint32(medianTimePast + timeLockDelta) sigScript, err = txscript.SignatureScript(tx, 0, testPkScript, txscript.SigHashAll, outputKey, true) if err != nil { t.Fatalf("unable to generate sig: %v", err) } tx.TxIn[0].SignatureScript = sigScript // If the time-lock delta is greater than -1, then the // transaction should be rejected from the mempool and when // included within a block. A time-lock delta of -1 should be // accepted as it has a lock-time of one // second _before_ the current MTP. _, err = r.Client.SendRawTransaction(tx, true) if err == nil && timeLockDelta >= 0 { t.Fatal("transaction was accepted into the mempool " + "but should be rejected!") } else if err != nil && !strings.Contains(err.Error(), "not finalized") { t.Fatalf("transaction should be rejected from mempool "+ "due to being non-final, instead: %v", err) } txns = []*btcutil.Tx{btcutil.NewTx(tx)} _, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{}) if err == nil && timeLockDelta >= 0 { t.Fatal("block should be rejected due to non-final " + "txn, but was accepted") } else if err != nil && !strings.Contains(err.Error(), "unfinalized") { t.Fatalf("block should be rejected due to non-final "+ "tx, instead: %v", err) } } } // createCSVOutput creates an output paying to a trivially redeemable CSV // pkScript with the specified time-lock. func createCSVOutput(r *rpctest.Harness, t *testing.T, numSatoshis btcutil.Amount, timeLock int32, isSeconds bool) ([]byte, *wire.OutPoint, *wire.MsgTx, error) { // Convert the time-lock to the proper sequence lock based according to // if the lock is seconds or time based. sequenceLock := blockchain.LockTimeToSequence(isSeconds, uint32(timeLock)) // Our CSV script is simply: OP_CSV OP_DROP b := txscript.NewScriptBuilder(). AddInt64(int64(sequenceLock)). AddOp(txscript.OP_CHECKSEQUENCEVERIFY). AddOp(txscript.OP_DROP) csvScript, err := b.Script() if err != nil { return nil, nil, nil, err } // Using the script generated above, create a P2SH output which will be // accepted into the mempool. p2shAddr, err := btcutil.NewAddressScriptHash(csvScript, r.ActiveNet) if err != nil { return nil, nil, nil, err } p2shScript, err := txscript.PayToAddrScript(p2shAddr) if err != nil { return nil, nil, nil, err } output := &wire.TxOut{ PkScript: p2shScript, Value: int64(numSatoshis), } // Finally create a valid transaction which creates the output crafted // above. tx, err := r.CreateTransaction([]*wire.TxOut{output}, 10, true) if err != nil { return nil, nil, nil, err } var outputIndex uint32 if !bytes.Equal(tx.TxOut[0].PkScript, p2shScript) { outputIndex = 1 } utxo := &wire.OutPoint{ Hash: tx.TxHash(), Index: outputIndex, } return csvScript, utxo, tx, nil } // spendCSVOutput spends an output previously created by the createCSVOutput // function. The sigScript is a trivial push of OP_TRUE followed by the // redeemScript to pass P2SH evaluation. func spendCSVOutput(redeemScript []byte, csvUTXO *wire.OutPoint, sequence uint32, targetOutput *wire.TxOut, txVersion int32) (*wire.MsgTx, error) { tx := wire.NewMsgTx(txVersion) tx.AddTxIn(&wire.TxIn{ PreviousOutPoint: *csvUTXO, Sequence: sequence, }) tx.AddTxOut(targetOutput) b := txscript.NewScriptBuilder(). AddOp(txscript.OP_TRUE). AddData(redeemScript) sigScript, err := b.Script() if err != nil { return nil, err } tx.TxIn[0].SignatureScript = sigScript return tx, nil } // assertTxInBlock asserts a transaction with the specified txid is found // within the block with the passed block hash. func assertTxInBlock(r *rpctest.Harness, t *testing.T, blockHash *chainhash.Hash, txid *chainhash.Hash) { block, err := r.Client.GetBlock(blockHash) if err != nil { t.Fatalf("unable to get block: %v", err) } if len(block.Transactions) < 2 { t.Fatal("target transaction was not mined") } for _, txn := range block.Transactions { txHash := txn.TxHash() if txn.TxHash() == txHash { return } } _, _, line, _ := runtime.Caller(1) t.Fatalf("assertion failed at line %v: txid %v was not found in "+ "block %v", line, txid, blockHash) } // TestBIP0068AndBIP0112Activation tests for the proper adherence to the BIP // 112 and BIP 68 rule-set after the activation of the CSV-package soft-fork. // // Overview: // - Pre soft-fork: // - A transaction spending a CSV output validly should be rejected from the // mempool, but accepted in a valid generated block including the // transaction. // - Post soft-fork: // - See the cases exercised within the table driven tests towards the end // of this test. func TestBIP0068AndBIP0112Activation(t *testing.T) { t.Parallel() // We'd like the test proper evaluation and validation of the BIP 68 // (sequence locks) and BIP 112 rule-sets which add input-age based // relative lock times. btcdCfg := []string{"--rejectnonstd"} r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "") if err != nil { t.Fatal("unable to create primary harness: ", err) } if err := r.SetUp(true, 1); err != nil { t.Fatalf("unable to setup test chain: %v", err) } defer r.TearDown() assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdStarted) harnessAddr, err := r.NewAddress() if err != nil { t.Fatalf("unable to obtain harness address: %v", err) } harnessScript, err := txscript.PayToAddrScript(harnessAddr) if err != nil { t.Fatalf("unable to generate pkScript: %v", err) } const ( outputAmt = btcutil.SatoshiPerBitcoin / 50 relativeBlockLock = 10 ) sweepOutput := &wire.TxOut{ Value: outputAmt - 5000, PkScript: harnessScript, } // As the soft-fork hasn't yet activated _any_ transaction version // which uses the CSV opcode should be accepted. Since at this point, // CSV doesn't actually exist, it's just a NOP. for txVersion := int32(0); txVersion < 3; txVersion++ { // Create a trivially spendable output with a CSV lock-time of // 10 relative blocks. redeemScript, testUTXO, tx, err := createCSVOutput(r, t, outputAmt, relativeBlockLock, false) if err != nil { t.Fatalf("unable to create CSV encumbered output: %v", err) } // As the transaction is p2sh it should be accepted into the // mempool and found within the next generated block. if _, err := r.Client.SendRawTransaction(tx, true); err != nil { t.Fatalf("unable to broadcast tx: %v", err) } blocks, err := r.Client.Generate(1) if err != nil { t.Fatalf("unable to generate blocks: %v", err) } txid := tx.TxHash() assertTxInBlock(r, t, blocks[0], &txid) // Generate a custom transaction which spends the CSV output. sequenceNum := blockchain.LockTimeToSequence(false, 10) spendingTx, err := spendCSVOutput(redeemScript, testUTXO, sequenceNum, sweepOutput, txVersion) if err != nil { t.Fatalf("unable to spend csv output: %v", err) } // This transaction should be rejected from the mempool since // CSV validation is already mempool policy pre-fork. _, err = r.Client.SendRawTransaction(spendingTx, true) if err == nil { t.Fatalf("transaction should have been rejected, but was " + "instead accepted") } // However, this transaction should be accepted in a custom // generated block as CSV validation for scripts within blocks // shouldn't yet be active. txns := []*btcutil.Tx{btcutil.NewTx(spendingTx)} block, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{}) if err != nil { t.Fatalf("unable to submit block: %v", err) } txid = spendingTx.TxHash() assertTxInBlock(r, t, block.Hash(), &txid) } // At this point, the block height should be 107: we started at height // 101, then generated 2 blocks in each loop iteration above. assertChainHeight(r, t, 107) // With the height at 107 we need 200 blocks to be mined after the // genesis target period, so we mine 192 blocks. This'll put us at // height 299. The getblockchaininfo call checks the state for the // block AFTER the current height. numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 8 if _, err := r.Client.Generate(numBlocks); err != nil { t.Fatalf("unable to generate blocks: %v", err) } assertChainHeight(r, t, 299) assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdActive) // Knowing the number of outputs needed for the tests below, create a // fresh output for use within each of the test-cases below. const relativeTimeLock = 512 const numTests = 8 type csvOutput struct { RedeemScript []byte Utxo *wire.OutPoint Timelock int32 } var spendableInputs [numTests]csvOutput // Create three outputs which have a block-based sequence locks, and // three outputs which use the above time based sequence lock. for i := 0; i < numTests; i++ { timeLock := relativeTimeLock isSeconds := true if i < 7 { timeLock = relativeBlockLock isSeconds = false } redeemScript, utxo, tx, err := createCSVOutput(r, t, outputAmt, int32(timeLock), isSeconds) if err != nil { t.Fatalf("unable to create CSV output: %v", err) } if _, err := r.Client.SendRawTransaction(tx, true); err != nil { t.Fatalf("unable to broadcast transaction: %v", err) } spendableInputs[i] = csvOutput{ RedeemScript: redeemScript, Utxo: utxo, Timelock: int32(timeLock), } } // Mine a single block including all the transactions generated above. if _, err := r.Client.Generate(1); err != nil { t.Fatalf("unable to generate block: %v", err) } // Now mine 10 additional blocks giving the inputs generated above a // age of 11. Space out each block 10 minutes after the previous block. prevBlockHash, err := r.Client.GetBestBlockHash() if err != nil { t.Fatalf("unable to get prior block hash: %v", err) } prevBlock, err := r.Client.GetBlock(prevBlockHash) if err != nil { t.Fatalf("unable to get block: %v", err) } for i := 0; i < relativeBlockLock; i++ { timeStamp := prevBlock.Header.Timestamp.Add(time.Minute * 10) b, err := r.GenerateAndSubmitBlock(nil, -1, timeStamp) if err != nil { t.Fatalf("unable to generate block: %v", err) } prevBlock = b.MsgBlock() } // A helper function to create fully signed transactions in-line during // the array initialization below. var inputIndex uint32 makeTxCase := func(sequenceNum uint32, txVersion int32) *wire.MsgTx { csvInput := spendableInputs[inputIndex] tx, err := spendCSVOutput(csvInput.RedeemScript, csvInput.Utxo, sequenceNum, sweepOutput, txVersion) if err != nil { t.Fatalf("unable to spend CSV output: %v", err) } inputIndex++ return tx } tests := [numTests]struct { tx *wire.MsgTx accept bool }{ // A valid transaction with a single input a sequence number // creating a 100 block relative time-lock. This transaction // should be rejected as its version number is 1, and only tx // of version > 2 will trigger the CSV behavior. { tx: makeTxCase(blockchain.LockTimeToSequence(false, 100), 1), accept: false, }, // A transaction of version 2 spending a single input. The // input has a relative time-lock of 1 block, but the disable // bit it set. The transaction should be rejected as a result. { tx: makeTxCase( blockchain.LockTimeToSequence(false, 1)|wire.SequenceLockTimeDisabled, 2, ), accept: false, }, // A v2 transaction with a single input having a 9 block // relative time lock. The referenced input is 11 blocks old, // but the CSV output requires a 10 block relative lock-time. // Therefore, the transaction should be rejected. { tx: makeTxCase(blockchain.LockTimeToSequence(false, 9), 2), accept: false, }, // A v2 transaction with a single input having a 10 block // relative time lock. The referenced input is 11 blocks old so // the transaction should be accepted. { tx: makeTxCase(blockchain.LockTimeToSequence(false, 10), 2), accept: true, }, // A v2 transaction with a single input having a 11 block // relative time lock. The input referenced has an input age of // 11 and the CSV op-code requires 10 blocks to have passed, so // this transaction should be accepted. { tx: makeTxCase(blockchain.LockTimeToSequence(false, 11), 2), accept: true, }, // A v2 transaction whose input has a 1000 blck relative time // lock. This should be rejected as the input's age is only 11 // blocks. { tx: makeTxCase(blockchain.LockTimeToSequence(false, 1000), 2), accept: false, }, // A v2 transaction with a single input having a 512,000 second // relative time-lock. This transaction should be rejected as 6 // days worth of blocks haven't yet been mined. The referenced // input doesn't have sufficient age. { tx: makeTxCase(blockchain.LockTimeToSequence(true, 512000), 2), accept: false, }, // A v2 transaction whose single input has a 512 second // relative time-lock. This transaction should be accepted as // finalized. { tx: makeTxCase(blockchain.LockTimeToSequence(true, 512), 2), accept: true, }, } for i, test := range tests { txid, err := r.Client.SendRawTransaction(test.tx, true) switch { // Test case passes, nothing further to report. case test.accept && err == nil: // Transaction should have been accepted but we have a non-nil // error. case test.accept && err != nil: t.Fatalf("test #%d, transaction should be accepted, "+ "but was rejected: %v", i, err) // Transaction should have been rejected, but it was accepted. case !test.accept && err == nil: t.Fatalf("test #%d, transaction should be rejected, "+ "but was accepted", i) // Transaction was rejected as wanted, nothing more to do. case !test.accept && err != nil: } // If the transaction should be rejected, manually mine a block // with the non-final transaction. It should be rejected. if !test.accept { txns := []*btcutil.Tx{btcutil.NewTx(test.tx)} _, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{}) if err == nil { t.Fatalf("test #%d, invalid block accepted", i) } continue } // Generate a block, the transaction should be included within // the newly mined block. blockHashes, err := r.Client.Generate(1) if err != nil { t.Fatalf("unable to mine block: %v", err) } assertTxInBlock(r, t, blockHashes[0], txid) } }