haskoin-core 1.6.0 → 1.6.1
raw patch · 4 files changed
+63/−24 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- haskoin-core.cabal +1/−1
- src/Haskoin/Address/Base58.hs +27/−4
- src/Haskoin/Address/CashAddr.hs +29/−19
CHANGELOG.md view
@@ -5,6 +5,12 @@ 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.1] - 2026-11-06++### Changed++- `encodeBase58`, `encodeBase58Check`, `cashAddrEncode`, `cash32encode` and `cash32encodeType`, and with them `addrToText` and the JSON form of addresses, are faster. Base58 divides its big integer by 58^10 instead of once per digit and builds its bytes directly instead of a `String`. CashAddr looks its characters up in a `ByteString` instead of with `(!!)` on a `String`, converts to five-bit groups without lists, and unrolls the generators of its checksum. A P2PKH address takes 1.1 µs instead of 5.6 µs in CashAddr and 4.3 µs instead of 5.9 µs in Base58. The output is the same: 120,061 encodings of random payloads are byte for byte the same before and after.+ ## [1.6.0] - 2026-11-06 ### Added
haskoin-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: haskoin-core-version: 1.6.0+version: 1.6.1 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/Address/Base58.hs view
@@ -33,9 +33,10 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as E-import Data.Word (Word8)+import Data.Bits (shiftL, (.|.))+import Data.Word (Word64, Word8) import Haskoin.Crypto.Hash (checkSum32)-import Haskoin.Util.Helpers (bsToInteger, integerToBS)+import Haskoin.Util.Helpers (integerToBS) import Numeric (showIntAtBase) -- | 'Base58' classic Bitcoin address format.@@ -71,7 +72,29 @@ -- | Encode an arbitrary-length 'Integer' into a 'Base58' string. Leading zeroes -- will not be part of the resulting string. encodeBase58I :: Integer -> Base58-encodeBase58I i = cs $ showIntAtBase 58 (chr . fromIntegral . b58) i ""+encodeBase58I i+ | i < 0 = cs $ showIntAtBase 58 (chr . fromIntegral . b58) i ""+ | otherwise = E.decodeLatin1 . B.pack . map b58 $ chunks i []+ where+ -- Divide by 58^10 rather than by 58, so that a number of n digits takes+ -- n/10 divisions of a big number and the rest is machine arithmetic.+ chunk = 58 ^ (10 :: Int) :: Integer+ chunks n acc+ | n < chunk = top (fromInteger n) acc+ | otherwise =+ let (q, r) = n `quotRem` chunk+ in chunks q (digits (10 :: Int) (fromInteger r) acc)+ -- the digits of a chunk, ten of them with leading zeros+ digits 0 _ acc = acc+ digits k w acc =+ let (q, r) = w `quotRem` (58 :: Word64)+ in digits (k - 1) q (fromIntegral r : acc)+ -- the digits of the leading chunk, without leading zeros+ top w acc+ | w < 58 = fromIntegral w : acc+ | otherwise =+ let (q, r) = w `quotRem` (58 :: Word64)+ in top q (fromIntegral r : acc) -- | Decode the UTF-8 bytes of a 'Base58' string into an 'Integer'. Fails on -- the first byte that is not a 'Base58' digit.@@ -93,7 +116,7 @@ l = T.replicate (B.length z) "1" -- preserve leading 0's r | B.null b = T.empty- | otherwise = encodeBase58I $ bsToInteger b+ | otherwise = encodeBase58I $ B.foldl' (\n w -> n `shiftL` 8 .|. toInteger w) 0 b -- | Decode a 'Base58'-encoded 'Text' to a 'ByteString'. Returns 'Nothing' if -- the input contains any character outside the 'Base58' alphabet.
src/Haskoin/Address/CashAddr.hs view
@@ -49,7 +49,7 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as T-import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Data.Text.Encoding (decodeLatin1, decodeUtf8, encodeUtf8) import Data.Word (Word64, Word8) import Haskoin.Network.Data (Network (cashAddrPrefix)) import Haskoin.Util.Helpers (convertBits)@@ -205,21 +205,28 @@ px = B.map (.&. 0x1f) (encodeUtf8 pfx) pd = px <> B.singleton 0 <> b32 <> B.replicate checkSumLength 0 cs = cash32Polymod pd- c32 = B.map f (b32 <> cs)- f = fromIntegral . ord . (charset !!) . fromIntegral- return (pfx <> ":" <> decodeUtf8 c32)+ c32 = B.map (B.index charsetBytes . fromIntegral) (b32 <> cs)+ return (pfx <> ":" <> decodeLatin1 c32) +-- | 'charset' as bytes, to look up characters in constant time.+charsetBytes :: ByteString+charsetBytes = C.pack charset+ -- | Convert base of 'ByteString' from eight bits per byte to five bits per -- byte, adding padding as necessary. toBase32 :: ByteString -> ByteString-toBase32 =- B.pack- . map fromIntegral- -- With padding allowed the conversion always succeeds.- . fromMaybe []- . convertBits True 8 5- . map fromIntegral- . B.unpack+toBase32 bs = fst $ B.unfoldrN groups group 0+ where+ groups = (B.length bs * 8 + 4) `div` 5+ byte i+ | i < B.length bs = fromIntegral (B.index bs i) :: Word+ | otherwise = 0+ -- the five bits that start this many bits into the input, from the two+ -- bytes that hold them, padded with zeros after the end+ group bit =+ let i = bit `shiftR` 3+ pair = (byte i `shiftL` 8) .|. byte (i + 1)+ in Just (fromIntegral ((pair `shiftR` (11 - (bit .&. 7))) .&. 0x1f), bit + 5) -- | Convert five-bit groups to the bytes they fill completely, dropping any -- bits left over at the end.@@ -270,17 +277,20 @@ cash32Polymod :: ByteString -> ByteString cash32Polymod v = B.pack- [fromIntegral (polymod `shiftR` (5 * (7 - i))) .&. 0x1f | i <- [0 .. 7]]+ [fromIntegral (polymod `shiftR` (5 * (7 - i))) .&. 0x1f | i <- [0 .. 7 :: Int]] where polymod = B.foldl' outer (1 :: Word64) v `xor` 1 outer c d =- let c0 = (fromIntegral (c `shiftR` 35) :: Word8)+ let c0 = c `shiftR` 35 c' = ((c .&. 0x07ffffffff) `shiftL` 5) `xor` fromIntegral d- in foldl' (inner c0) c' (zip [0 ..] generator)- generator =- [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470]- inner c0 c (b, g)- | c0 `testBit` b = c `xor` g+ in g 4 0x1e4f43e470 c0+ . g 3 0xae2eabe2a8 c0+ . g 2 0xf33e5fb3c4 c0+ . g 1 0x79b76d99e2 c0+ $ g 0 0x98f2bc8e61 c0 c'+ g :: Int -> Word64 -> Word64 -> Word64 -> Word64+ g b gen c0 c+ | c0 `testBit` b = c `xor` gen | otherwise = c -- | Validate that polymod 'ByteString' (eight bytes) is equal to zero.