Skip to content

Commit f4bcd1d

Browse files
committed
better bitcoin address parser for bolt11
1 parent 66750f8 commit f4bcd1d

File tree

4 files changed

+75
-15
lines changed

4 files changed

+75
-15
lines changed

pub/functora/functora.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ common pkg-bolt11
250250
, base
251251
, base16-bytestring
252252
, bech32
253+
, bitcoin-address
253254
, bytestring
254255
, text
255256

@@ -362,6 +363,7 @@ test-suite functora-test
362363
build-depends:
363364
, base
364365
, base16-bytestring
366+
, bitcoin-address
365367
, bytestring
366368
, containers
367369
, Crypto

pub/functora/src/bolt11/Functora/Bolt11.hs

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
{-# LANGUAGE CPP #-}
33

44
module Functora.Bolt11
5-
( Hex (..),
5+
( Word5,
6+
Hex (..),
67
inspectHex,
78
Tag (..),
89
isPaymentHash,
@@ -17,6 +18,10 @@ module Functora.Bolt11
1718
)
1819
where
1920

21+
import qualified Bitcoin.Address as Btc
22+
import qualified Bitcoin.Address.Hash as Btc
23+
import qualified Bitcoin.Address.SegWit as SegWit
24+
import qualified Bitcoin.Address.Settings as Btc
2025
import Codec.Binary.Bech32 (Word5)
2126
import qualified Codec.Binary.Bech32.Internal as Bech32
2227
import Control.Applicative
@@ -75,7 +80,7 @@ data Tag
7580
| DescriptionHash Hex
7681
| Expiry Int
7782
| MinFinalCltvExpiry Int
78-
| OnchainFallback -- TODO: address type
83+
| OnchainFallback Btc.Address
7984
| ExtraRouteInfo
8085
| FeatureBits [Word5]
8186
| UnknownTag Int [Word5]
@@ -93,6 +98,21 @@ data Network
9398
| BitcoinSignet
9499
deriving stock (Eq, Ord, Show, Data, Generic)
95100

101+
networkSettings :: Network -> Btc.Settings
102+
networkSettings = \case
103+
BitcoinMainnet -> Btc.btc
104+
BitcoinTestnet -> Btc.btcTestnet
105+
BitcoinRegtest ->
106+
Btc.btcTestnet
107+
{ Btc.settings_prefixSegWit =
108+
fromMaybe (error "Bad prefixSegWit") $ Btc.prefixSegWit "bcrt"
109+
}
110+
BitcoinSignet ->
111+
Btc.btcTestnet
112+
{ Btc.settings_prefixSegWit =
113+
fromMaybe (error "Bad prefixSegWit") $ Btc.prefixSegWit "tbs"
114+
}
115+
96116
data Multiplier = Milli | Micro | Nano | Pico
97117
deriving stock (Eq, Ord, Show, Data, Generic)
98118

@@ -220,13 +240,43 @@ w5int bytes = foldl' decodeInt 0 (zip [0 ..] (take 7 (reverse bytes)))
220240
decodeInt !n (i, byte) =
221241
n .|. fromEnum byte `shiftL` (i * 5)
222242

223-
parseTag :: [Word5] -> (Maybe Tag, [Word5])
224-
parseTag [] = (Nothing, [])
225-
parseTag ws@[_] = (Nothing, ws)
226-
parseTag ws@[_, _] = (Nothing, ws) -- appease the compiler warning gods
227-
parseTag ws
243+
w5addr :: Network -> [Word5] -> Either String Btc.Address
244+
w5addr _ [] = Left "Empty Onchain Fallback Address"
245+
w5addr net (v0 : rest) =
246+
case fromEnum v0 of
247+
-- SegWit
248+
vsn | vsn <= 16 -> do
249+
sv0 <- first displayException $ tryFrom @Int @Word8 vsn
250+
sv1 <-
251+
maybe
252+
(Left $ "Bad SegWit verion " <> inspect sv0)
253+
Right
254+
$ SegWit.version sv0
255+
raw <- w5bs rest
256+
res <- maybe (Left "Bad SegWit program") Right $ SegWit.program sv1 raw
257+
pure $ Btc.SegWit (Btc.settings_prefixSegWit cfg) res
258+
-- P2PKH
259+
17 -> do
260+
raw <- w5bs rest
261+
res <- maybe (Left "Bad PubHash160") Right $ Btc.parsePubHash160 raw
262+
pure $ Btc.P2PKH (Btc.settings_prefixP2PKH cfg) res
263+
-- P2SH
264+
18 -> do
265+
raw <- w5bs rest
266+
res <- maybe (Left "Bad ScriptHash160") Right $ Btc.parseScriptHash160 raw
267+
pure $ Btc.P2SH (Btc.settings_prefixP2SH cfg) res
268+
vsn -> do
269+
Left $ "Bad Onchain Fallback verion " <> inspect vsn
270+
where
271+
cfg = networkSettings net
272+
273+
parseTag :: Network -> [Word5] -> (Maybe Tag, [Word5])
274+
parseTag _ [] = (Nothing, [])
275+
parseTag _ ws@[_] = (Nothing, ws)
276+
parseTag _ ws@[_, _] = (Nothing, ws) -- appease the compiler warning gods
277+
parseTag _ ws
228278
| length ws < 8 = (Nothing, ws)
229-
parseTag ws@(t0 : d1 : d2 : rest)
279+
parseTag net ws@(t0 : d1 : d2 : rest)
230280
| length rest < 7 = (Nothing, ws)
231281
| otherwise = (Just tag, leftovers)
232282
where
@@ -247,7 +297,7 @@ parseTag ws@(t0 : d1 : d2 : rest)
247297
23 -> mkt DescriptionHash hex
248298
6 -> Expiry $ w5int dat
249299
24 -> MinFinalCltvExpiry $ w5int dat
250-
9 -> OnchainFallback
300+
9 -> mkt OnchainFallback $ w5addr net dat
251301
3 -> ExtraRouteInfo
252302
5 -> FeatureBits dat
253303
n -> UnknownTag n dat
@@ -257,22 +307,22 @@ data MSig
257307
| Unk [Word5]
258308
deriving stock (Eq, Ord, Show, Generic)
259309

260-
parseTags :: [Word5] -> ([Tag], MSig)
261-
parseTags ws
310+
parseTags :: Network -> [Word5] -> ([Tag], MSig)
311+
parseTags net ws
262312
| length ws == 104 = ([], Sig ws)
263313
| otherwise =
264-
let (mtag, rest) = parseTag ws
314+
let (mtag, rest) = parseTag net ws
265315
in maybe
266316
([], Unk rest)
267-
(\tag -> first (tag :) (parseTags rest))
317+
(\tag -> first (tag :) (parseTags net rest))
268318
mtag
269319

270320
decodeBolt11 :: Text -> Either String Bolt11
271321
decodeBolt11 raw = do
272322
(rawHrp, rawDp) <- first show $ Bech32.decodeLenient raw
273323
hrp <- Atto.parseOnly parseHrp $ Bech32.humanReadablePartToText rawHrp
274324
let (ts, rest) = splitAt 7 $ Bech32.dataPartToWords rawDp
275-
let (tags, leftover) = parseTags rest
325+
let (tags, leftover) = parseTags (bolt11HrpNet hrp) rest
276326
(rawSig, recFlag) <- case leftover of
277327
Sig ws -> Right $ splitAt 103 ws
278328
Unk left -> Left ("corrupt, leftover: " <> show left)

pub/functora/src/functora-ghcjs.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ common pkg
9090
, bech32
9191
, binary
9292
, binary-instances
93+
, bitcoin-address
9394
, bmp
9495
, bytestring
9596
, containers

pub/functora/src/test/Functora/Bolt11Spec.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
module Functora.Bolt11Spec (spec) where
22

3+
import qualified Bitcoin.Address as Btc
4+
import qualified Bitcoin.Address.Hash as Btc
5+
import qualified Bitcoin.Address.Settings as Btc
36
import Functora.Bolt11
47
import Functora.Prelude
58
import Test.Hspec
@@ -45,7 +48,11 @@ goodSamples =
4548
[ PaymentHash "0001020304050607080900010203040506070809000102030405060708090102",
4649
DescriptionHash
4750
"3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1",
48-
OnchainFallback,
51+
OnchainFallback
52+
. Btc.P2PKH (Btc.PrefixP2PKH 0)
53+
. fromMaybe (error "BADHASH")
54+
. Btc.parsePubHash160
55+
$ unHex "04b61f7dc1ea0dc99424464cc4064dc564d91e89",
4956
ExtraRouteInfo
5057
],
5158
bolt11Signature =

0 commit comments

Comments
 (0)