haskoin-core 1.6.3 → 1.6.4
raw patch · 7 files changed
+145/−11 lines, 7 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Haskoin.Util.Helpers: encodeHexBytes :: ByteString -> ByteString
+ Haskoin.Util.Helpers: encodeHexReversed :: ShortByteString -> Text
+ Haskoin.Util.Helpers: hexEncodingReversed :: ShortByteString -> Encoding
Files
- CHANGELOG.md +22/−0
- README.md +1/−1
- haskoin-core.cabal +1/−1
- src/Haskoin/Block/Common.hs +3/−2
- src/Haskoin/Transaction/Common.hs +2/−3
- src/Haskoin/Util/Helpers.hs +90/−3
- test/Haskoin/UtilSpec.hs +26/−1
CHANGELOG.md view
@@ -5,6 +5,28 @@ 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.6.4] - 2026-11-14++### Changed++- **Hex is written faster.** `encodeHex`, `encodeHexLazy`, `hexBuilder` and+ `hexEncoding` look up the two digits of each byte in a table. On a+ 250-byte transaction, that is four times as fast as base16, which+ `encodeHex` used, and on a block of a megabyte, 1.5 times as fast as the+ builder of bytestring, which `hexBuilder` used. Short input inside a+ builder is still written by the builder's own hex, which is as fast+ there and allocates nothing.+- **Hashes of blocks and transactions are written five times as fast in+ JSON**: 78 ns where it took 390, straight from their bytes, without+ serializing and reversing them first. `txHashToHex`, `blockHashToHex`+ and the hashes in a block header gain too.++### Added++- `encodeHexBytes`, the hex of bytes as the bytes of its digits.+- `encodeHexReversed` and `hexEncodingReversed`, the hex of bytes in+ reverse order, as hashes are shown, as text and as JSON.+ ## [1.6.3] - 2026-11-14 ### Changed
README.md view
@@ -115,7 +115,7 @@ ```yaml resolver: nightly-2026-10-28 extra-deps:- - haskoin-core-1.6.3+ - haskoin-core-1.6.4 - base16-2.0 - secp256k1-haskell-2.0.1 ```
haskoin-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: haskoin-core-version: 1.6.3+version: 1.6.4 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
src/Haskoin/Block/Common.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-}@@ -152,11 +153,11 @@ -- | Hex of a 256-bit hash in display order, which is the reverse of its byte -- order in memory. Block hashes and Merkle roots are shown this way. reversedHex :: Hash256 -> Text-reversedHex = encodeHex . B.reverse . runPutS . serialize+reversedHex = encodeHexReversed . (.get) -- | JSON encoding of a 256-bit hash in display order. reversedHexEncoding :: Hash256 -> Encoding-reversedHexEncoding = hexEncoding . L.reverse . runPutL . serialize+reversedHexEncoding = hexEncodingReversed . (.get) -- | Parse hex in display order into a 256-bit hash. Any input that is not -- exactly 32 bytes of hex is rejected instead of being truncated.
src/Haskoin/Transaction/Common.hs view
@@ -108,7 +108,7 @@ instance ToJSON TxHash where toJSON = A.String . txHashToHex- toEncoding = hexEncoding . BL.reverse . runPutL . serialize+ toEncoding (TxHash h) = hexEncodingReversed h.get -- | Hash of the transaction with every input script and the whole witness -- removed, so that it is the same before and after signing, whether the@@ -122,8 +122,7 @@ -- | Convert transaction hash to hex form, reversing bytes. txHashToHex :: TxHash -> Text-txHashToHex (TxHash h) =- encodeHex . B.reverse . runPutS $ serialize h+txHashToHex (TxHash h) = encodeHexReversed h.get -- | Convert transaction hash from hex, reversing bytes. The hex must be -- exactly 32 bytes long, as @uint256::FromHex@ requires in Bitcoin Core.
src/Haskoin/Util/Helpers.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-}@@ -19,7 +20,10 @@ hexEncoding, hexBuilder, encodeHex,+ encodeHexBytes, encodeHexLazy,+ encodeHexReversed,+ hexEncodingReversed, decodeHex, decodeHexLazy, getBits,@@ -82,8 +86,14 @@ import Data.ByteString qualified as B import Data.ByteString.Base16 import Data.ByteString.Builder+import Data.ByteString.Builder.Prim qualified as BP+import Data.ByteString.Builder.Prim.Internal qualified as BPI+import Data.ByteString.Internal qualified as BI import Data.ByteString.Lazy qualified as LB import Data.ByteString.Lazy.Base16 qualified as LB16+import Data.ByteString.Short (ShortByteString)+import Data.ByteString.Short qualified as SBS+import Data.ByteString.Unsafe qualified as BU import Data.Bytes.Get import Data.Bytes.Put import Data.Bytes.Serial@@ -98,6 +108,8 @@ import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LT import Data.Word+import Foreign.Ptr (Ptr)+import Foreign.Storable (peekByteOff, pokeByteOff) import Test.Hspec -- ByteString helpers@@ -118,20 +130,95 @@ f 0 = Nothing f x = Just (fromInteger x :: Word8, x `shiftR` 8) +-- Hexadecimal+--+-- Hex is written by looking up the two digits of each byte in a table of+-- the 256 pairs. Measured on a 250-byte transaction, that is four times as+-- fast as base16, and twice as fast as the hex of the builder of+-- bytestring, which works a digit at a time; on a block of a megabyte, it+-- is 1.5 times as fast as the builder. Inside a builder, short input is+-- left to the builder's own hex, which costs as much there and writes in+-- place: copying in a table-made string would allocate for each one.++-- | JSON string of the hex of bytes. hexEncoding :: LB.ByteString -> Encoding hexEncoding b = unsafeToEncoding $ char7 '"' <> hexBuilder b <> char7 '"' +-- | Hex of bytes, in lowercase, to append to other output. hexBuilder :: LB.ByteString -> Builder-hexBuilder = lazyByteStringHex+hexBuilder = foldMap chunk . LB.toChunks+ where+ chunk c+ | B.length c < 256 = byteStringHex c+ | otherwise = byteString (encodeHexBytes c) +-- | Hex of bytes, in lowercase. encodeHex :: ByteString -> Text-encodeHex = extractBase16 . encodeBase16+encodeHex = T.decodeLatin1 . encodeHexBytes +-- | Hex of bytes, in lowercase, as the bytes of its ASCII digits.+encodeHexBytes :: ByteString -> ByteString+encodeHexBytes bs =+ BI.unsafeCreate (2 * n) $ \out ->+ BU.unsafeUseAsCString bs $ \inp ->+ let go !i+ | i >= n = return ()+ | otherwise = do+ w <- peekByteOff inp i+ pokeDigits out (2 * i) w+ go (i + 1)+ in go 0+ where+ n = B.length bs+ -- | Encode as string of human-readable hex characters. encodeHexLazy :: LB.ByteString -> LT.Text-encodeHexLazy = extractBase16 . LB16.encodeBase16+encodeHexLazy = LT.fromChunks . map encodeHex . LB.toChunks++-- | Hex of bytes in reverse order, as hashes are shown, in lowercase.+encodeHexReversed :: ShortByteString -> Text+encodeHexReversed h =+ T.decodeLatin1 . BI.unsafeCreate (2 * n) $ \out ->+ let go !i+ | i >= n = return ()+ | otherwise = pokeDigits out (2 * i) (SBS.index h (n - 1 - i)) >> go (i + 1)+ in go 0+ where+ n = SBS.length h++-- | JSON string of the hex of bytes in reverse order, as hashes are shown.+-- A hash of 32 bytes, the size of the hashes of blocks and transactions,+-- is written in one step, straight from its bytes: a fifth of the time of+-- serializing it, reversing it and writing its hex.+hexEncodingReversed :: ShortByteString -> Encoding+hexEncodingReversed h+ | SBS.length h == 32 = unsafeToEncoding (BP.primFixed quotedHash h)+ | otherwise = hexEncoding (LB.fromStrict (B.reverse (SBS.fromShort h)))+ where+ quotedHash = BPI.fixedPrim 66 $ \x p -> do+ pokeByteOff p 0 (0x22 :: Word8)+ let go !i+ | i >= 32 = return ()+ | otherwise = pokeDigits p (1 + 2 * i) (SBS.index x (31 - i)) >> go (i + 1)+ go 0+ pokeByteOff p 65 (0x22 :: Word8)++-- | Write the two hex digits of a byte at an offset, from the table.+pokeDigits :: Ptr a -> Int -> Word8 -> IO ()+pokeDigits out at w = do+ let k = 2 * fromIntegral w+ pokeByteOff out at (BU.unsafeIndex hexDigits k)+ pokeByteOff out (at + 1) (BU.unsafeIndex hexDigits (k + 1))+{-# INLINE pokeDigits #-}++-- | The two lowercase hex digits of each byte, in order: 512 bytes.+hexDigits :: ByteString+hexDigits = B.pack (concat [[digit (w `shiftR` 4), digit (w .&. 15)] | w <- [0 .. 255]])+ where+ digit x = if x < 10 then 0x30 + x else 0x57 + x+{-# NOINLINE hexDigits #-} decodeHex :: Text -> Maybe ByteString decodeHex t =
test/Haskoin/UtilSpec.hs view
@@ -15,8 +15,11 @@ import Data.Aeson.Encoding (encodingToLazyByteString) import Data.ByteString (ByteString) import Data.ByteString qualified as B-import Data.ByteString.Builder (toLazyByteString)+import Data.ByteString.Builder (byteStringHex, lazyByteStringHex, toLazyByteString) import Data.ByteString.Lazy qualified as BL+import Data.ByteString.Short qualified as SBS+import Data.Text.Encoding qualified as TE+import Data.Text.Lazy.Encoding qualified as TLE import Data.Bytes.Get import Data.Bytes.Put import Data.Default (def)@@ -101,6 +104,28 @@ encodingToLazyByteString (hexEncoding "\xab\xcd") `shouldBe` "\"abcd\"" toLazyByteString (hexBuilder "\xab\xcd") `shouldBe` "abcd" prop "decodeHex . encodeHex" $ forAll arbitraryBS fromToHex+ -- the table-based hex against the hex of the builder of bytestring,+ -- byte by byte, on every byte value and on chunks of every size+ -- either side of where the builder's own hex takes over+ it "writes every byte value as the builder of bytestring does" $ do+ let every = B.pack [0 .. 255]+ reference = BL.toStrict (toLazyByteString (byteStringHex every))+ encodeHexBytes every `shouldBe` reference+ encodeHex every `shouldBe` TE.decodeLatin1 reference+ prop "writes what the builder of bytestring writes, whatever the chunks" $+ forAll (listOf (sized (\n -> B.pack <$> vectorOf (n * 7) arbitrary))) $ \chunks -> do+ let lazy = BL.fromChunks chunks+ reference = toLazyByteString (lazyByteStringHex lazy)+ toLazyByteString (hexBuilder lazy) `shouldBe` reference+ encodeHexLazy lazy `shouldBe` TLE.decodeLatin1 reference+ encodingToLazyByteString (hexEncoding lazy) `shouldBe` "\"" <> reference <> "\""+ prop "writes reversed hex as the hex of the reversed bytes" $+ forAll (oneof [pure 32, choose (0, 70)]) $ \n -> forAll (vectorOf n arbitrary) $ \ws -> do+ let bytes = B.pack ws+ reversed = BL.toStrict (toLazyByteString (byteStringHex (B.reverse bytes)))+ encodeHexReversed (SBS.toShort bytes) `shouldBe` TE.decodeLatin1 reversed+ encodingToLazyByteString (hexEncodingReversed (SBS.toShort bytes))+ `shouldBe` BL.fromStrict ("\"" <> reversed <> "\"") prop "decodeHexLazy . encodeHexLazy" $ forAll arbitraryBS $ \bs -> decodeHexLazy (encodeHexLazy (BL.fromStrict bs)) == Just (BL.fromStrict bs) describe "getBits" $ do