packages feed

haskoin-node-2.5.0: test/unit/Haskoin/Node/BlockSpec.hs

-- | Whether a block holds the transactions its header commits to, against
-- blocks mined by Bitcoin Core 32.1 and copies of them changed the ways a
-- peer could change them.
module Haskoin.Node.BlockSpec (spec) where

import Data.ByteString qualified as B
import Data.Serialize (encode)
import Haskoin
import Haskoin.Node
import Haskoin.Node.Test (corePreSegWitBlock, coreSegWitBlock)
import Test.Hspec

-- | The same block with the witness of every transaction removed.
stripWitness :: Block -> Block
stripWitness b = b {txs = [tx {witness = []} | tx <- b.txs]}

-- | The same block with the last byte of the last witness item of its
-- second transaction changed. Only the witness changes, so the txid and the
-- Merkle root stay the same.
alterWitness :: Block -> Block
alterWitness b = case b.txs of
  cb : tx : rest -> b {txs = cb : tx {witness = map (map flipLast) tx.witness} : rest}
  _ -> b
  where
    flipLast item
      | B.null item = item
      | otherwise = B.init item <> B.singleton (B.last item + 1)

-- | A node at the given height, standing for the parent of a block.
parentAt :: Network -> BlockHeight -> BlockNode
parentAt net h =
  BlockNode
    { header = net.genesisHeader,
      height = h,
      work = 0,
      skip = headerHash net.genesisHeader
    }

-- | A transaction that is not a coinbase, with an output script of the
-- given length. Without witness it is 60 bytes plus the script: 4 version
-- + 1 input count + 41 input + 1 output count + 9 output value and script
-- length + 4 lock time = 60.
nonCoinbase :: Int -> Tx
nonCoinbase n =
  Tx
    { version = 1,
      inputs =
        [ TxIn
            { outpoint = OutPoint {hash = spent, index = 0},
              script = B.empty,
              sequence = 0
            }
        ],
      outputs = [TxOut {value = 1, script = B.replicate n 0x51}],
      witness = [],
      locktime = 0
    }
  where
    spent = "1111111111111111111111111111111111111111111111111111111111111111"

-- | A block with the given transactions under a header that commits to them.
blockOf :: [Tx] -> Block
blockOf txs =
  Block
    { header =
        btcRegTest.genesisHeader
          { merkle = either error id (buildMerkleRoot (map txHash txs))
          },
      txs = txs
    }

spec :: Spec
spec = do
  describe "Bitcoin Core blocks used as fixtures" $ do
    it "are the blocks that Bitcoin Core printed" $ do
      headerHash corePreSegWitBlock.header
        `shouldBe` "55e949faadd10136287852156f9078c4ba8cadd1b602697ff4202b395bd71d75"
      headerHash coreSegWitBlock.header
        `shouldBe` "30f221284bb2468933c8a37992771939bf4257cebfb4191f7c0fab5703a62efb"
      map txHash coreSegWitBlock.txs
        `shouldBe` [ "7581bf22701c376882dac3d3d70a86e9341a35eb32bcd797f47619907f54a81e",
                     "9d304fed9ba3333c3cf6c23739376e5b740b3c181ebd08578dab63dc6401d23d",
                     "8ba9b11d4a6a205d5dd4625c71f2fb7292c08926a87c5f622bf38c09980f49c4"
                   ]
      map txWitnessHash coreSegWitBlock.txs
        `shouldBe` [ "67188a253a1f92310c43db8ebc98cb5b3b6cb26668710402f2362e29193b2758",
                     "a6b593b3a88dba0efcfe44d125eaf1c8413be672519300d001458197256a158e",
                     "8ba9b11d4a6a205d5dd4625c71f2fb7292c08926a87c5f622bf38c09980f49c4"
                   ]

  describe "blockMutation" $ do
    it "accepts a SegWit block that Bitcoin Core mined" $
      blockMutation SegWitActive coreSegWitBlock `shouldBe` Nothing

    it "accepts a block mined before SegWit with a commitment but no coinbase witness" $
      blockMutation SegWitInactive corePreSegWitBlock `shouldBe` Nothing

    it "would refuse that block if SegWit applied to it, so the height must be known" $
      -- Core's miner adds the commitment before activation, but not the
      -- 32-byte value in the coinbase witness that goes with it.
      blockMutation SegWitActive corePreSegWitBlock `shouldBe` Just BadWitnessNonceSize

    it "refuses a block whose transactions do not hash to the Merkle root" $ do
      -- Bitcoin Core: bad-txnmrklroot
      let b = coreSegWitBlock {txs = take 2 coreSegWitBlock.txs}
      blockMutation SegWitActive b `shouldBe` Just BadMerkleRoot

    it "refuses a block that repeats its last transaction to keep the Merkle root" $ do
      -- CVE-2012-2459. Bitcoin Core: bad-txns-duplicate. Three transactions
      -- and the same three with the last one repeated give the same root,
      -- because an odd level pairs its last node with itself.
      let b = coreSegWitBlock {txs = coreSegWitBlock.txs <> drop 2 coreSegWitBlock.txs}
      length b.txs `shouldBe` 4
      blockMutation SegWitActive b `shouldBe` Just DuplicateTransactions

    it "refuses a block without transactions" $ do
      let b = coreSegWitBlock {txs = []}
      blockMutation SegWitActive b `shouldBe` Just BadMerkleRoot

    it "refuses a SegWit block whose witness data was stripped" $
      -- The coinbase loses its 32-byte reserved value first.
      blockMutation SegWitActive (stripWitness coreSegWitBlock)
        `shouldBe` Just BadWitnessNonceSize

    it "refuses a SegWit block whose witness data was changed" $
      -- Bitcoin Core gave bad-witness-merkle-match for such a block.
      blockMutation SegWitActive (alterWitness coreSegWitBlock)
        `shouldBe` Just BadWitnessCommitment

    it "refuses witness data in a block for which SegWit is not active" $
      -- Bitcoin Core: unexpected-witness
      blockMutation SegWitInactive coreSegWitBlock `shouldBe` Just UnexpectedWitness

    it "does not check the witness data of a block whose height is unknown" $
      blockMutation WitnessUnknown (alterWitness coreSegWitBlock) `shouldBe` Nothing

    it "still checks the Merkle root of a block whose height is unknown" $ do
      let b = coreSegWitBlock {txs = take 2 coreSegWitBlock.txs}
      blockMutation WitnessUnknown b `shouldBe` Just BadMerkleRoot

    it "refuses a block without a coinbase that holds a 64-byte transaction" $ do
      -- 60 + 4 script bytes = 64
      B.length (encode (nonCoinbase 4)) `shouldBe` 64
      blockMutation SegWitActive (blockOf [nonCoinbase 4])
        `shouldBe` Just SixtyFourByteTransaction

    it "leaves other blocks without a coinbase to block validation" $ do
      -- 60 + 5 script bytes = 65
      B.length (encode (nonCoinbase 5)) `shouldBe` 65
      blockMutation SegWitActive (blockOf [nonCoinbase 5]) `shouldBe` Nothing

  describe "witnessCheck" $ do
    it "applies the witness rule on Bitcoin from block 481824" $ do
      witnessCheck btc (Just (parentAt btc 481822)) `shouldBe` SegWitInactive
      witnessCheck btc (Just (parentAt btc 481823)) `shouldBe` SegWitActive

    it "applies the witness rule on testnet3 from block 834624" $ do
      witnessCheck btcTest (Just (parentAt btcTest 834622)) `shouldBe` SegWitInactive
      witnessCheck btcTest (Just (parentAt btcTest 834623)) `shouldBe` SegWitActive

    it "applies the witness rule on testnet4 and regtest from the first block" $ do
      witnessCheck btcTest4 (Just (parentAt btcTest4 0)) `shouldBe` SegWitActive
      witnessCheck btcRegTest (Just (parentAt btcRegTest 0)) `shouldBe` SegWitActive

    it "leaves the witness unchecked when the parent is unknown" $
      witnessCheck btc Nothing `shouldBe` WitnessUnknown

    it "allows no witness data on Bitcoin Cash, whatever the height" $ do
      witnessCheck bch Nothing `shouldBe` SegWitInactive
      witnessCheck bch (Just (parentAt bch 800000)) `shouldBe` SegWitInactive