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 +18/−0
- haskoin-core.cabal +5/−5
- src/Haskoin/Block/Headers.hs +30/−7
- src/Haskoin/Network/Constants.hs +91/−31
- src/Haskoin/Network/Data.hs +6/−2
- src/Haskoin/Util/Helpers.hs +8/−12
- test/Haskoin/BlockSpec.hs +85/−0
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 ->