diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/haskoin-core.cabal b/haskoin-core.cabal
--- a/haskoin-core.cabal
+++ b/haskoin-core.cabal
@@ -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
diff --git a/src/Haskoin/Block/Headers.hs b/src/Haskoin/Block/Headers.hs
--- a/src/Haskoin/Block/Headers.hs
+++ b/src/Haskoin/Block/Headers.hs
@@ -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
diff --git a/src/Haskoin/Network/Constants.hs b/src/Haskoin/Network/Constants.hs
--- a/src/Haskoin/Network/Constants.hs
+++ b/src/Haskoin/Network/Constants.hs
@@ -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]
diff --git a/src/Haskoin/Network/Data.hs b/src/Haskoin/Network/Data.hs
--- a/src/Haskoin/Network/Data.hs
+++ b/src/Haskoin/Network/Data.hs
@@ -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
diff --git a/src/Haskoin/Util/Helpers.hs b/src/Haskoin/Util/Helpers.hs
--- a/src/Haskoin/Util/Helpers.hs
+++ b/src/Haskoin/Util/Helpers.hs
@@ -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
diff --git a/test/Haskoin/BlockSpec.hs b/test/Haskoin/BlockSpec.hs
--- a/test/Haskoin/BlockSpec.hs
+++ b/test/Haskoin/BlockSpec.hs
@@ -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 ->
