packages feed

haskoin-core 1.3.0 → 1.4.0

raw patch · 7 files changed

+243/−57 lines, 7 filesdep ~base16dep ~secp256k1-haskellPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base16, secp256k1-haskell

API changes (from Hackage documentation)

+ Haskoin.Block.Headers: validTimeWarp :: Network -> BlockHeight -> Timestamp -> Timestamp -> Bool
+ Haskoin.Network.Constants: btcTest4 :: Network
+ Haskoin.Network.Data: [bip94] :: Network -> !Bool
- Haskoin.Network.Data: Network :: !String -> !Word8 -> !Word8 -> !Word8 -> !Word32 -> !Word32 -> !Word32 -> !BlockHeader -> !Int -> !Word64 -> !ByteString -> !Int -> !Bool -> !Bool -> !Integer -> !(BlockHeight, BlockHash) -> !BlockHeight -> !BlockHeight -> !Word32 -> !Word32 -> ![(BlockHeight, BlockHash)] -> !Word32 -> ![String] -> !Maybe Word32 -> !Maybe Word32 -> !Maybe Word32 -> !Maybe Word32 -> !Integer -> !Bool -> !Maybe Text -> !Maybe Text -> !Bool -> !Word32 -> Network
+ Haskoin.Network.Data: Network :: !String -> !Word8 -> !Word8 -> !Word8 -> !Word32 -> !Word32 -> !Word32 -> !BlockHeader -> !Int -> !Word64 -> !ByteString -> !Int -> !Bool -> !Bool -> !Bool -> !Integer -> !(BlockHeight, BlockHash) -> !BlockHeight -> !BlockHeight -> !Word32 -> !Word32 -> ![(BlockHeight, BlockHash)] -> !Word32 -> ![String] -> !Maybe Word32 -> !Maybe Word32 -> !Maybe Word32 -> !Maybe Word32 -> !Integer -> !Bool -> !Maybe Text -> !Maybe Text -> !Bool -> !Word32 -> Network

Files

CHANGELOG.md view
@@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2026-10-31++### Added++- Bitcoin testnet4 network (`btctest4`) with the BIP94 block storm and time warp rules.++### Changed++- Update DNS seeds to match Bitcoin Core and Bitcoin Cash Node. Drop the Bitcoin ABC seeds, which now serve eCash.+- This package now requires base16 2.0 and secp256k1-haskell 0.7.++### Fixed++- Compute the difficulty retarget in `nextPowWorkRequired` and `nextEdaWorkRequired`, which never reached their retarget branches.+- Fix the Bitcoin Cash DAA activation height, which had a typo (404031 instead of 504032).+- Start the DAA one block later on Bitcoin Cash testnet and testnet4, to match Bitcoin Cash Node.+- Fix the Bitcoin Cash ASERT half-life, which must be two days.+ ## [1.3.0] - 2026-08-12  ### Changed
haskoin-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           haskoin-core-version:        1.3.0+version:        1.4.0 synopsis:       Bitcoin & Bitcoin Cash library for Haskell description:    Please see the README on GitHub at <https://github.com/jprupp/haskoin-core#readme> category:       Bitcoin, Finance, Network@@ -108,7 +108,7 @@     , aeson >=1.4.6.0     , array >=0.5.4.0     , base >=4.9 && <5-    , base16 >=1.0+    , base16 >=2.0     , binary >=0.8.8     , bytes >=0.17     , bytestring >=0.10.10.0@@ -127,7 +127,7 @@     , ram >=0.22.0     , safe >=0.3.18     , scientific >=0.3.6.2-    , secp256k1-haskell >=1.5.0+    , secp256k1-haskell >=1.7.0     , split >=0.2.3.3     , string-conversions >=0.4.0.1     , text >=1.2.3.0@@ -165,7 +165,7 @@     , aeson >=1.4.6.0     , array >=0.5.4.0     , base >=4.9 && <5-    , base16 >=1.0+    , base16 >=2.0     , base64     , binary >=0.8.8     , bytes >=0.17@@ -188,7 +188,7 @@     , ram >=0.22.0     , safe >=0.3.18     , scientific >=0.3.6.2-    , secp256k1-haskell >=1.5.0+    , secp256k1-haskell >=1.7.0     , split >=0.2.3.3     , string-conversions >=0.4.0.1     , text >=1.2.3.0
src/Haskoin/Block/Headers.hs view
@@ -52,6 +52,7 @@     afterLastCP,     bip34,     validVersion,+    validTimeWarp,     lastNoMinDiff,     nextWorkRequired,     nextEdaWorkRequired,@@ -404,6 +405,9 @@   unless (nt >= mt) $     Left $       "Block timestamp too early: " ++ show nt+  unless (validTimeWarp net ng par.header.timestamp nt) $+    Left $+      "Block timestamp too early for retarget: " ++ show nt   unless (afterLastCP net (bb.height) ng) $     Left $       "Rewriting pre-checkpoint chain: " ++ show ng@@ -522,6 +526,21 @@   | version < 4 = height < net.bip65Height   | otherwise = True +-- | BIP94 time warp rule: the first block of a retarget period may not be more+-- than 600 seconds earlier than its parent.+validTimeWarp ::+  Network ->+  -- | new child height+  BlockHeight ->+  -- | parent timestamp+  Timestamp ->+  -- | new child timestamp+  Timestamp ->+  Bool+validTimeWarp net height parent child+  | net.bip94 && height `mod` diffInterval net == 0 = child + 600 >= parent+  | otherwise = True+ -- | Find last block with normal, as opposed to minimum difficulty (for test -- networks). lastNoMinDiff :: (BlockHeaders m) => Network -> BlockNode -> m BlockNode@@ -573,8 +592,8 @@ nextEdaWorkRequired ::   (BlockHeaders m) => Network -> BlockNode -> BlockHeader -> m Word32 nextEdaWorkRequired net par bh-  | par.height + 1 `mod` diffInterval net == 0 =-      nextWorkRequired net par bh+  | (par.height + 1) `mod` diffInterval net == 0 =+      nextPowWorkRequired net par bh   | mindiff = return (encodeCompact net.powLimit)   | par.header.bits == encodeCompact net.powLimit =       return (encodeCompact net.powLimit)@@ -806,7 +825,7 @@ nextPowWorkRequired ::   (BlockHeaders m) => Network -> BlockNode -> BlockHeader -> m Word32 nextPowWorkRequired net par bh-  | par.height + 1 `mod` diffInterval net /= 0 =+  | (par.height + 1) `mod` diffInterval net /= 0 =       if net.minDiffBlocks         then           if ht > pt + delta@@ -816,10 +835,14 @@               return d.header.bits         else return par.header.bits   | otherwise = do-      let rh = par.height - diffInterval net - 1-      a <- fromMaybe e1 <$> getAncestor rh par-      let t = a.header.timestamp-      return $ calcNextWork net par.header t+      -- First block of the retarget period that ends with the parent.+      f <- fromMaybe e1 <$> getAncestor (par.height - (diffInterval net - 1)) par+      -- BIP94 bases the retarget on the first block of the period, which+      -- cannot use the min-difficulty exception, instead of the last one.+      let h+            | net.bip94 = (par.header :: BlockHeader) {bits = f.header.bits}+            | otherwise = par.header+      return $ calcNextWork net h f.header.timestamp   where     e1 = error "Could not get ancestor for block header"     pt = par.header.timestamp
src/Haskoin/Network/Constants.hs view
@@ -18,6 +18,7 @@   ( -- * Constants     btc,     btcTest,+    btcTest4,     btcRegTest,     bch,     bchTest,@@ -87,6 +88,7 @@       defaultPort = 8333,       minDiffBlocks = False,       powNoRetarget = False,+      bip94 = False,       powLimit =         0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -139,15 +141,13 @@           )         ],       seeds =-        [ "seed.bitcoin.sipa.be", -- Pieter Wuille-          "dnsseed.bluematt.me", -- Matt Corallo-          "dnsseed.bitcoin.dashjr.org", -- Luke Dashjr-          "seed.bitcoinstats.com", -- Chris Decker+        [ "dnsseed.bluematt.me", -- Matt Corallo           "seed.bitcoin.jonasschnelli.ch", -- Jonas Schnelli-          "seed.btc.petertodd.org", -- Peter Todd+          "seed.btc.petertodd.net", -- Peter Todd           "seed.bitcoin.sprovoost.nl", -- Sjors Provoost           "dnsseed.emzy.de", -- Stephan Oeste-          "seed.bitcoin.wiz.biz" -- Jason Maurice+          "seed.bitcoin.wiz.biz", -- Jason Maurice+          "seed.mainnet.achownodes.xyz" -- Ava Chow         ],       bip44Coin = 0,       sigHashForkId = Nothing,@@ -188,6 +188,7 @@       defaultPort = 18333,       minDiffBlocks = True,       powNoRetarget = False,+      bip94 = False,       powLimit =         0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -204,10 +205,10 @@           )         ],       seeds =-        [ "testnet-seed.bitcoin.jonasschnelli.ch",-          "seed.tbtc.petertodd.org",-          "seed.testnet.bitcoin.sprovoost.nl",-          "testnet-seed.bluematt.me"+        [ "testnet-seed.bitcoin.jonasschnelli.ch", -- Jonas Schnelli+          "seed.tbtc.petertodd.net", -- Peter Todd+          "testnet-seed.bluematt.me", -- Matt Corallo+          "seed.testnet.achownodes.xyz" -- Ava Chow         ],       bip44Coin = 1,       sigHashForkId = Nothing,@@ -222,6 +223,61 @@       halvingInterval = 210000     } +-- | Testnet4 for Bitcoin SegWit network (BIP94).+btcTest4 :: Network+btcTest4 =+  Network+    { name = "btctest4",+      addrPrefix = 111,+      scriptPrefix = 196,+      secretPrefix = 239,+      xPubPrefix = 0x043587cf,+      xPrvPrefix = 0x04358394,+      magic = 0x1c163f28,+      genesisHeader =+        BlockHeader+          0x01+          "0000000000000000000000000000000000000000000000000000000000000000"+          "4e7b2b9128fe0291db0693af2ae418b767e657cd407e80cb1434221eaea7a07a"+          1714777860+          0x1d00ffff+          393743547,+      -- Hash 00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043+      maxBlockSize = 1000000,+      maxSatoshi = 2100000000000000,+      userAgent = "/haskoin-btc-test4:" <> versionString <> "/",+      defaultPort = 48333,+      minDiffBlocks = True,+      powNoRetarget = False,+      bip94 = True,+      powLimit =+        0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff,+      bip34Block =+        ( 1,+          "0000000012982b6d5f621229286b880e909984df669c2afabb102ce311b13f28"+        ),+      bip65Height = 1,+      bip66Height = 1,+      targetTimespan = 14 * 24 * 60 * 60,+      targetSpacing = 10 * 60,+      checkpoints = [],+      seeds =+        [ "seed.testnet4.bitcoin.sprovoost.nl", -- Sjors Provoost+          "seed.testnet4.wiz.biz" -- Jason Maurice+        ],+      bip44Coin = 1,+      sigHashForkId = Nothing,+      edaHeight = Nothing,+      daaHeight = Nothing,+      asertActivationTime = Nothing,+      asertHalfLife = 0,+      segWit = True,+      cashAddrPrefix = Nothing,+      bech32Prefix = Just "tb",+      replaceByFee = True,+      halvingInterval = 210000+    }+ -- | RegTest for Bitcoin SegWit network. btcRegTest :: Network btcRegTest =@@ -248,6 +304,7 @@       defaultPort = 18444,       minDiffBlocks = True,       powNoRetarget = True,+      bip94 = False,       powLimit =         0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -299,6 +356,7 @@       defaultPort = 8333,       minDiffBlocks = False,       powNoRetarget = False,+      bip94 = False,       powLimit =         0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -361,19 +419,19 @@           )         ],       seeds =-        [ "seed.bitcoinabc.org",-          "seed-bch.bitcoinforks.org",-          "btccash-seeder.bitcoinunlimited.info",-          "seed.bchd.cash",-          "seed.bch.loping.net",-          "dnsseed.electroncash.de"+        [ "seed.flowee.cash", -- Flowee+          "btccash-seeder.bitcoinunlimited.info", -- Bitcoin Unlimited+          "seed.bchd.cash", -- BCHD+          "seed.bch.loping.net", -- Loping.net+          "bchseed.c3-soft.com", -- C3 Soft+          "bch.bitjson.com" -- Jason Dreyzehner         ],       bip44Coin = 145,       sigHashForkId = Just 0,       edaHeight = Just 478559,-      daaHeight = Just 404031,+      daaHeight = Just 504032,       asertActivationTime = Just 1605441600,-      asertHalfLife = 60 * 60 * 10,+      asertHalfLife = 2 * 24 * 60 * 60,       segWit = False,       cashAddrPrefix = Just "bitcoincash",       bech32Prefix = Nothing,@@ -381,7 +439,7 @@       halvingInterval = 210000     } --- | Testnet for Bitcoin Cash network.+-- | Testnet4 for Bitcoin Cash network. bchTest4 :: Network bchTest4 =   Network@@ -400,13 +458,14 @@           1597811185           0x1d00ffff           114152193,-      -- Hash 000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943+      -- Hash 000000001dd410c49a788668ce26751718cc797474d3152a5fc073dd44fd9f7b       maxBlockSize = 2000000,       maxSatoshi = 2100000000000000,       userAgent = "/haskoin-bch-test4:" <> versionString <> "/",       defaultPort = 28333,       minDiffBlocks = True,       powNoRetarget = False,+      bip94 = False,       powLimit =         0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -434,15 +493,15 @@           )         ],       seeds =-        [ "testnet4-seed-bch.bitcoinforks.org",-          "testnet4-seed-bch.toom.im",-          "seed.tbch4.loping.net",-          "testnet4-seed.flowee.cash"+        [ "testnet4-seed-bch.toom.im", -- Jonathan Toomim+          "seed.tbch4.loping.net", -- Loping.net+          "testnet4-seed.flowee.cash", -- Flowee+          "testnet4.bitjson.com" -- Jason Dreyzehner         ],       bip44Coin = 1,       sigHashForkId = Just 0,       edaHeight = Just 7,-      daaHeight = Just 3000,+      daaHeight = Just 3001,       asertActivationTime = Just 1605441600,       asertHalfLife = 60 * 60,       segWit = False,@@ -478,6 +537,7 @@       defaultPort = 18333,       minDiffBlocks = True,       powNoRetarget = False,+      bip94 = False,       powLimit =         0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -504,15 +564,14 @@           )         ],       seeds =-        [ "testnet-seed.bitcoinabc.org",-          "testnet-seed-bch.bitcoinforks.org",-          "testnet-seed.bchd.cash",-          "seed.tbch.loping.net"+        [ "testnet-seed.bchd.cash", -- BCHD+          "seed.tbch.loping.net", -- Loping.net+          "testnet-seed.bitcoinunlimited.info" -- Bitcoin Unlimited         ],       bip44Coin = 1,       sigHashForkId = Just 0,       edaHeight = Just 1155876,-      daaHeight = Just 1188697,+      daaHeight = Just 1188698,       asertActivationTime = Just 1605441600,       asertHalfLife = 60 * 60,       segWit = False,@@ -548,6 +607,7 @@       defaultPort = 18444,       minDiffBlocks = True,       powNoRetarget = True,+      bip94 = False,       powLimit =         0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,       bip34Block =@@ -579,4 +639,4 @@  -- | List of all networks supported by this library. allNets :: [Network]-allNets = [btc, bch, btcTest, bchTest4, bchTest, btcRegTest, bchRegTest]+allNets = [btc, bch, btcTest, btcTest4, bchTest4, bchTest, btcRegTest, bchRegTest]
src/Haskoin/Network/Data.hs view
@@ -46,6 +46,8 @@     minDiffBlocks :: !Bool,     -- | do not retarget difficulty (regtest)     powNoRetarget :: !Bool,+    -- | enforce BIP94 block storm and time warp rules (testnet4)+    bip94 :: !Bool,     -- | proof-of-work target higest possible value     powLimit :: !Integer,     -- | block at which BIP34 activates@@ -66,9 +68,11 @@     seeds :: ![String],     -- | fork id for replay protection     sigHashForkId :: !(Maybe Word32),-    -- | EDA start block height+    -- | height of the first block that uses the EDA+    -- (one above @uahfHeight@ in Bitcoin Cash Node)     edaHeight :: !(Maybe Word32),-    -- | DAA start block height+    -- | height of the first block that uses the DAA+    -- (one above @daaHeight@ in Bitcoin Cash Node)     daaHeight :: !(Maybe Word32),     -- | asert3-2d algorithm activation time     -- TODO: Replace with block height after fork
src/Haskoin/Util/Helpers.hs view
@@ -135,18 +135,18 @@  decodeHex :: Text -> Maybe ByteString decodeHex t =-  if isBase16 u8-    then Just . decodeBase16 $ assertBase16 u8-    else Nothing+  case parseBase16 u8 of+    Right b16 -> Just $ decodeBase16 b16+    Left _ -> Nothing   where     u8 = T.encodeUtf8 t  -- | Decode string of human-readable hex characters. decodeHexLazy :: LT.Text -> Maybe LB.ByteString decodeHexLazy t =-  if LB16.isBase16 u8-    then Just . LB16.decodeBase16 $ assertBase16 u8-    else Nothing+  case LB16.parseBase16 u8 of+    Right b16 -> Just $ LB16.decodeBase16 b16+    Left _ -> Nothing   where     u8 = LT.encodeUtf8 t @@ -402,12 +402,8 @@ --  prepareContext :: (Ctx -> SpecWith a) -> SpecWith a-prepareContext go = do-  ctx <- runIO $ do-    ctx <- createContext-    randomizeContext ctx-    return ctx-  afterAll_ (destroyContext ctx) (go ctx)+prepareContext go = go =<< runIO createContext+    customCerealID :: (Eq a) => S.Get a -> S.Putter a -> a -> Bool customCerealID g p a = (S.runGet g . S.runPut . p) a == Right a
test/Haskoin/BlockSpec.hs view
@@ -73,6 +73,28 @@   where     bhs = appendBlocks net 6 bh i +-- | Regtest with the BIP94 rules and a retarget every ten blocks.+bip94Net :: Network+bip94Net =+  (btcRegTest :: Network)+    { bip94 = True,+      powNoRetarget = False,+      targetTimespan = 10 * 600+    }++-- | The same network without the BIP94 rules.+noBip94Net :: Network+noBip94Net = (bip94Net :: Network) {bip94 = False}++-- | Mine a header on top of the parent with the given timestamp and bits.+mineOn :: Network -> BlockHeader -> Timestamp -> Word32 -> BlockHeader+mineOn net parent time bits =+  mineBlock net 0 (parent :: BlockHeader) {prev = headerHash parent, timestamp = time, bits = bits}++-- | Mine a chain of headers with the given timestamps and bits.+mineChain :: Network -> BlockHeader -> [(Timestamp, Word32)] -> [BlockHeader]+mineChain net parent = drop 1 . scanl (\p (time, bits) -> mineOn net p time bits) parent+ spec :: Spec spec = prepareContext $ \ctx -> do   testIdentity $ identityTests ctx@@ -97,6 +119,69 @@       let net = bchRegTest           bb = withChain net $ splitChain net >> getBestBlockHeader        in bb.height `shouldBe` 4035+  describe "testnet4" $ do+    it "has the BIP94 genesis block" $+      headerHash btcTest4.genesisHeader+        `shouldBe` "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043"+    it "has the Bitcoin Cash testnet4 genesis block" $+      headerHash bchTest4.genesisHeader+        `shouldBe` "000000001dd410c49a788668ce26751718cc797474d3152a5fc073dd44fd9f7b"+    it "rejects the first block of a retarget period when it is too early" $ do+      -- Blocks 1 to 8 are ten minutes apart; block 9 jumps two hours ahead.+      let t = bip94Net.genesisHeader.timestamp+          hs =+            mineChain bip94Net bip94Net.genesisHeader $+              [(t + 600 * i, 0x207fffff) | i <- [1 .. 8]] ++ [(t + 12000, 0x207fffff)]+          par = last hs+          b10 = mineOn bip94Net par (t + 11400) 0x207fffff+          connect net chain' h = withChain net $ do+            connectBlocks net myTime chain' >>= either error (const (return ()))+            fmap (.height) <$> connectBlock net myTime h+      -- Block 10 starts a retarget period, so it may be at most 600 seconds+      -- earlier than block 9.+      connect bip94Net hs (mineOn bip94Net par (t + 11399) 0x207fffff)+        `shouldBe` Left "Block timestamp too early for retarget: 1296700001"+      connect bip94Net hs b10 `shouldBe` Right 10+      -- Block 11 does not start a period.+      connect bip94Net (hs ++ [b10]) (mineOn bip94Net b10 (t + 10799) 0x207fffff)+        `shouldBe` Right 11+      connect noBip94Net hs (mineOn noBip94Net par (t + 11399) 0x207fffff)+        `shouldBe` Right 10+    it "bases the retarget on the first block of the period" $ do+      -- Blocks 10 to 19 form a retarget period that takes four times the+      -- target timespan. Its last block uses the min-difficulty exception.+      let t = bip94Net.genesisHeader.timestamp+          hs =+            mineChain bip94Net bip94Net.genesisHeader $+              [(t + 600 * i, 0x207fffff) | i <- [1 .. 9]]+                ++ [(t + 6000 + 600 * i, 0x1f7fffff) | i <- [0 .. 8]]+                ++ [(t + 30000, 0x207fffff)]+          next net = withChain net $ do+            connectBlocks net myTime hs >>= either error (const (return ()))+            par <- getBestBlockHeader+            let bh = (par.header :: BlockHeader) {prev = headerHash par.header, timestamp = t + 30600}+            nextWorkRequired net par bh+      -- Four times the difficulty of block 10.+      next bip94Net `shouldBe` 0x2001ffff+      -- Without BIP94, block 19 drags the next period down to min difficulty.+      next noBip94Net `shouldBe` 0x207fffff+  describe "emergency difficulty adjustment" $+    it "retargets at the end of a period" $ do+      -- Blocks 10 to 19 form a retarget period that takes four times the+      -- target timespan.+      let net = (noBip94Net :: Network) {edaHeight = Just 1}+          t = net.genesisHeader.timestamp+          hs =+            mineChain net net.genesisHeader $+              [(t + 600 * i, 0x207fffff) | i <- [1 .. 9]]+                ++ [(t + 6000 + 600 * i, 0x1f7fffff) | i <- [0 .. 8]]+                ++ [(t + 30000, 0x1f7fffff)]+          next = withChain net $ do+            connectBlocks net myTime hs >>= either error (const (return ()))+            par <- getBestBlockHeader+            let bh = (par.header :: BlockHeader) {prev = headerHash par.header, timestamp = t + 30600}+            nextWorkRequired net par bh+      next `shouldBe` 0x2001ffff   describe "block hash" $ do     prop "encodes and decodes block hash" $       forAll arbitraryBlockHash $ \h ->