commit 5e84a235ed9ceb051653fba1fd2d09661a70839d
parent bba3bab650ff572546b91c3d3330203a259331a5
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 14:54:04 +0400
test: rewrite the suite against the public API
Assert onion-test.json, onion-error-test.json, the Returning Errors
trace, route-blinding-test.json and blinded-payment-onion-test.json
through the public API only. Bob's and Eve's encrypted_data, which carry
unknown odd TLVs, are now reproduced exactly, and the blinded payment is
processed end to end with each hop's path_key.
Add negative tests for each rejection rule (key validation, packet and
payload decoding, the path key rules of process, failure message
decoding, construction limits) and round-trip properties for the
codecs, error wrapping and multi-hop routes.
Diffstat:
| M | test/Main.hs | | | 3554 | ++++++++++++++++++++++++++++++------------------------------------------------- |
1 file changed, 1363 insertions(+), 2191 deletions(-)
diff --git a/test/Main.hs b/test/Main.hs
@@ -2,1706 +2,867 @@
module Main where
-import Control.Monad (foldM)
+import Control.Monad (forM_)
+import qualified Crypto.Cipher.ChaCha20 as ChaCha
+import qualified Crypto.Curve.Secp256k1 as Secp256k1
+import qualified Crypto.Hash.SHA256 as SHA256
import Data.Bits (xor)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as B16
-import qualified Crypto.Curve.Secp256k1 as Secp256k1
-import qualified Crypto.Hash.SHA256 as SHA256
-import Data.List (nub, sort)
-import Data.Maybe (isJust)
-import Data.Word (Word8, Word16, Word32)
-import Lightning.Protocol.BOLT4.Blinding
-import Lightning.Protocol.BOLT4.Codec
-import Lightning.Protocol.BOLT4.Construct
-import Lightning.Protocol.BOLT4.Error
-import Lightning.Protocol.BOLT4.Internal
-import Lightning.Protocol.BOLT4.Prim
-import Lightning.Protocol.BOLT4.Process
-import Lightning.Protocol.BOLT4.Types
+import Data.List (nubBy)
+import Data.Word (Word8, Word16, Word32, Word64)
+import qualified Lightning.Protocol.BOLT1 as BOLT1
+import Lightning.Protocol.BOLT4
+import qualified Lightning.Protocol.BOLT9 as BOLT9
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import qualified Vectors as V
--- | Demand a Just value in IO, failing the test on Nothing.
-demand :: String -> Maybe a -> IO a
-demand _ (Just a) = pure a
-demand msg Nothing = assertFailure msg
-
--- | Decode hex, failing the test on invalid input.
-unhex :: BS.ByteString -> IO BS.ByteString
-unhex = demand "invalid hex" . B16.decode
-
--- | Decode a hex-encoded public key, failing the test if invalid.
-unhexPoint :: BS.ByteString -> IO Secp256k1.Projective
-unhexPoint h = unhex h >>= demand "invalid point" . Secp256k1.parse_point
-
--- | Strip a BigSize length prefix, checking that it is exact.
-unprefix :: BS.ByteString -> IO BS.ByteString
-unprefix bs = do
- (len, body) <- demand "invalid length prefix" (decodeBigSize bs)
- assertEqual "length prefix" (fromIntegral len) (BS.length body)
- pure body
-
--- | Construct a ShortChannelId, failing if invalid.
-assertScid :: Word32 -> Word32 -> Word16
- -> IO ShortChannelId
-assertScid b t o = demand "shortChannelId" (shortChannelId b t o)
-
--- | Construct a ShortChannelId for test fixtures.
-mkScid :: Word32 -> Word32 -> Word16 -> ShortChannelId
-mkScid b t o = case shortChannelId b t o of
- Just s -> s
- Nothing -> error "mkScid: invalid test fixture"
-
main :: IO ()
main = defaultMain $ testGroup "ppad-bolt4" [
- testGroup "Prim" [
- primTests
- ]
- , testGroup "BigSize" [
- bigsizeTests
- , bigsizeRoundtripProp
- ]
- , testGroup "TLV" [
- tlvTests
- , tlvRejectionTests
- ]
- , testGroup "ShortChannelId" [
- sciTests
- ]
- , testGroup "OnionPacket" [
- onionPacketTests
- ]
- , testGroup "Construct" [
- constructTests
- ]
- , testGroup "Process" [
- processTests
- ]
- , testGroup "Error" [
- errorTests
- ]
- , testGroup "properties" [
- propertyTests
- ]
- , testGroup "Blinding" [
- blindingKeyDerivationTests
- , blindingEphemeralKeyTests
- , blindingTlvTests
- , blindingEncryptionTests
- , blindingCreatePathTests
- , blindingProcessHopTests
- ]
+ key_tests
+ , codec_tests
+ , failure_tests
+ , construct_tests
+ , process_tests
+ , blinding_tests
+ , error_tests
, testGroup "spec vectors" [
- onionVectorTests
- , errorVectorTests
- , traceVectorTests
- , routeBlindingTests
- , blindedPaymentTests
+ onion_vectors
+ , error_vectors
+ , trace_vectors
+ , route_blinding_vectors
+ , blinded_payment_vectors
]
- , testGroup "multi-hop" [
- multiHopTests
- ]
- ]
-
--- BigSize tests ------------------------------------------------------------
-
-bigsizeTests :: TestTree
-bigsizeTests = testGroup "boundary values" [
- testCase "0" $
- encodeBigSize 0 @?= BS.pack [0x00]
- , testCase "0xFC" $
- encodeBigSize 0xFC @?= BS.pack [0xFC]
- , testCase "0xFD" $
- encodeBigSize 0xFD @?= BS.pack [0xFD, 0x00, 0xFD]
- , testCase "0xFFFF" $
- encodeBigSize 0xFFFF @?= BS.pack [0xFD, 0xFF, 0xFF]
- , testCase "0x10000" $
- encodeBigSize 0x10000 @?=
- BS.pack [0xFE, 0x00, 0x01, 0x00, 0x00]
- , testCase "0xFFFFFFFF" $
- encodeBigSize 0xFFFFFFFF @?=
- BS.pack [0xFE, 0xFF, 0xFF, 0xFF, 0xFF]
- , testCase "0x100000000" $
- encodeBigSize 0x100000000 @?=
- BS.pack [0xFF, 0x00, 0x00, 0x00, 0x01,
- 0x00, 0x00, 0x00, 0x00]
- , testCase "decode 0" $ do
- let result = decodeBigSize (BS.pack [0x00])
- result @?= Just (0, BS.empty)
- , testCase "decode 0xFC" $ do
- let result = decodeBigSize (BS.pack [0xFC])
- result @?= Just (0xFC, BS.empty)
- , testCase "decode 0xFD" $ do
- let result = decodeBigSize (BS.pack [0xFD, 0x00, 0xFD])
- result @?= Just (0xFD, BS.empty)
- , testCase "decode 0xFFFF" $ do
- let result = decodeBigSize (BS.pack [0xFD, 0xFF, 0xFF])
- result @?= Just (0xFFFF, BS.empty)
- , testCase "decode 0x10000" $ do
- let result = decodeBigSize $
- BS.pack [0xFE, 0x00, 0x01, 0x00, 0x00]
- result @?= Just (0x10000, BS.empty)
- , testCase "decode 0xFFFFFFFF" $ do
- let result = decodeBigSize $
- BS.pack [0xFE, 0xFF, 0xFF, 0xFF, 0xFF]
- result @?= Just (0xFFFFFFFF, BS.empty)
- , testCase "decode 0x100000000" $ do
- let result = decodeBigSize $
- BS.pack [0xFF, 0x00, 0x00, 0x00, 0x01,
- 0x00, 0x00, 0x00, 0x00]
- result @?= Just (0x100000000, BS.empty)
- , testCase "reject non-canonical 0xFD encoding" $ do
- let result = decodeBigSize (BS.pack [0xFD, 0x00, 0xFC])
- result @?= Nothing
- , testCase "reject non-canonical 0xFE encoding" $ do
- let result = decodeBigSize $
- BS.pack [0xFE, 0x00, 0x00, 0xFF, 0xFF]
- result @?= Nothing
- , testCase "bigSizeLen" $ do
- bigSizeLen 0 @?= 1
- bigSizeLen 0xFC @?= 1
- bigSizeLen 0xFD @?= 3
- bigSizeLen 0xFFFF @?= 3
- bigSizeLen 0x10000 @?= 5
- bigSizeLen 0xFFFFFFFF @?= 5
- bigSizeLen 0x100000000 @?= 9
- ]
-
-bigsizeRoundtripProp :: TestTree
-bigsizeRoundtripProp = testProperty "roundtrip" $ \n ->
- let encoded = encodeBigSize n
- decoded = decodeBigSize encoded
- in decoded == Just (n, BS.empty)
-
--- TLV tests ----------------------------------------------------------------
-
-tlvTests :: TestTree
-tlvTests = testGroup "encoding/decoding" [
- testCase "single record" $ do
- let rec = TlvRecord 2 (BS.pack [0x01, 0x02, 0x03])
- encoded = encodeTlv rec
- decoded = decodeTlv encoded
- decoded @?= Just (rec, BS.empty)
- , testCase "stream roundtrip" $ do
- let recs = [ TlvRecord 2 (BS.pack [0x01])
- , TlvRecord 4 (BS.pack [0x02, 0x03])
- , TlvRecord 100 (BS.pack [0x04, 0x05, 0x06])
- ]
- encoded = encodeTlvStream recs
- decoded = decodeTlvStream encoded
- decoded @?= Just recs
- , testCase "reject out-of-order types" $ do
- let rec1 = encodeTlv (TlvRecord 4 (BS.pack [0x01]))
- rec2 = encodeTlv (TlvRecord 2 (BS.pack [0x02]))
- badStream = rec1 <> rec2
- decoded = decodeTlvStream badStream
- decoded @?= Nothing
- , testCase "reject duplicate types" $ do
- let rec1 = encodeTlv (TlvRecord 2 (BS.pack [0x01]))
- rec2 = encodeTlv (TlvRecord 2 (BS.pack [0x02]))
- badStream = rec1 <> rec2
- decoded = decodeTlvStream badStream
- decoded @?= Nothing
- , testCase "empty stream" $ do
- let decoded = decodeTlvStream BS.empty
- decoded @?= Just []
- ]
-
-tlvRejectionTests :: TestTree
-tlvRejectionTests = testGroup "rejection" [
- testCase "length of 2^64 - 1" $ do
- let huge = BS.pack (0xff : replicate 8 0xff)
- decodeTlv (BS.singleton 0x01 <> huge <> BS.pack [0xaa, 0xbb])
- @?= Nothing
- decodeTlvStream (BS.singleton 0x01 <> huge <> BS.pack [0x03, 0x00])
- @?= Nothing
- decodeHopPayload
- (BS.singleton 0x02 <> huge <> BS.pack [0x04, 0x01, 0x07])
- @?= Nothing
- , testCase "hop payload: unknown even type" $
- decodeHopPayload (BS.pack [0x02, 0x01, 0x05, 0x0e, 0x01, 0x07])
- @?= Nothing
- , testCase "hop payload: unknown odd type is kept" $
- fmap hpUnknownTlvs
- (decodeHopPayload (BS.pack [0x02, 0x01, 0x05, 0x0f, 0x01, 0x07]))
- @?= Just [TlvRecord 15 (BS.singleton 0x07)]
- , testCase "hop payload: types 16 and 18 are accepted" $
- fmap hpUnknownTlvs
- (decodeHopPayload (BS.pack [0x10, 0x01, 0x01, 0x12, 0x01, 0x02]))
- @?= Just [ TlvRecord 16 (BS.singleton 0x01)
- , TlvRecord 18 (BS.singleton 0x02) ]
- , testCase "blinded hop data: unknown even type" $
- decodeBlindedHopData (BS.pack [0x10, 0x01, 0x00]) @?= Nothing
- , testCase "blinded hop data: unknown odd type is skipped" $
- decodeBlindedHopData (BS.pack [0x06, 0x01, 0x2a, 0xfd, 0x02, 0x31,
- 0x01, 0x00])
- @?= Just emptyHopData { bhdPathId = Just (BS.singleton 0x2a) }
- ]
-
--- ShortChannelId tests -----------------------------------------------------
-
-sciTests :: TestTree
-sciTests = testGroup "encoding/decoding" [
- testCase "known value" $ do
- sci <- assertScid 700000 1234 5
- let encoded = encodeShortChannelId sci
- BS.length encoded @?= 8
- let decoded = decodeShortChannelId encoded
- decoded @?= Just sci
- , testCase "maximum values" $ do
- sci <- assertScid 0xFFFFFF 0xFFFFFF 0xFFFF
- let encoded = encodeShortChannelId sci
- BS.length encoded @?= 8
- let decoded = decodeShortChannelId encoded
- decoded @?= Just sci
- , testCase "zero values" $ do
- sci <- assertScid 0 0 0
- let encoded = encodeShortChannelId sci
- expected = BS.pack [0, 0, 0, 0, 0, 0, 0, 0]
- encoded @?= expected
- let decoded = decodeShortChannelId encoded
- decoded @?= Just sci
- , testCase "reject wrong length" $ do
- let decoded =
- decodeShortChannelId (BS.pack [0, 1, 2, 3, 4, 5, 6])
- decoded @?= Nothing
- ]
-
--- OnionPacket tests --------------------------------------------------------
-
-onionPacketTests :: TestTree
-onionPacketTests = testGroup "encoding/decoding" [
- testCase "roundtrip" $ do
- let packet = OnionPacket
- { opVersion = 0x00
- , opEphemeralKey = BS.replicate 33 0xAB
- , opHopPayloads =
- unsafeHopPayloads (BS.replicate 1300 0xCD)
- , opHmac = unsafeHmac32 (BS.replicate 32 0xEF)
- }
- encoded = encodeOnionPacket packet
- BS.length encoded @?= onionPacketSize
- let decoded = decodeOnionPacket encoded
- decoded @?= Just packet
- , testCase "reject wrong size" $ do
- let decoded =
- decodeOnionPacket (BS.replicate 1000 0x00)
- decoded @?= Nothing
+ , properties
]
--- Prim tests ---------------------------------------------------------------
+-- helpers --------------------------------------------------------------------
-sessionKey :: BS.ByteString
-sessionKey = BS.replicate 32 0x41
-
-hop0PubKeyHex :: BS.ByteString
-hop0PubKeyHex =
- "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"
-
-hop0SharedSecretHex :: BS.ByteString
-hop0SharedSecretHex =
- "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66"
-
-hop0BlindingFactorHex :: BS.ByteString
-hop0BlindingFactorHex =
- "2ec2e5da605776054187180343287683aa6a51b4b1c04d6dd49c45d8cffb3c36"
-
-fromHex :: BS.ByteString -> BS.ByteString
-fromHex h = case B16.decode h of
- Just bs -> bs
- Nothing -> error "fromHex: invalid hex"
-
-primTests :: TestTree
-primTests = testGroup "cryptographic primitives" [
- testSharedSecret
- , testBlindingFactor
- , testKeyDerivation
- , testBlindPubKey
- , testGenerateStream
- , testHmacOperations
- ]
-
-testSharedSecret :: TestTree
-testSharedSecret =
- testCase "computeSharedSecret (BOLT4 spec hop 0)" $ do
- pubKey <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- case computeSharedSecret sessionKey pubKey of
- Nothing ->
- assertFailure "computeSharedSecret returned Nothing"
- Just (SharedSecret computed) -> do
- let expected = fromHex hop0SharedSecretHex
- computed @?= expected
-
-testBlindingFactor :: TestTree
-testBlindingFactor =
- testCase "computeBlindingFactor (BOLT4 spec hop 0)" $ do
- sk <- demand "roll32" $ Secp256k1.roll32 sessionKey
- ephemPubKey <- demand "derive_pub" $
- Secp256k1.derive_pub sk
- nodePubKey <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- case computeSharedSecret sessionKey nodePubKey of
- Nothing ->
- assertFailure "computeSharedSecret returned Nothing"
- Just sharedSecret -> do
- let BlindingFactor computed =
- computeBlindingFactor ephemPubKey sharedSecret
- expected = fromHex hop0BlindingFactorHex
- computed @?= expected
-
-testKeyDerivation :: TestTree
-testKeyDerivation = testGroup "key derivation" [
- testCase "deriveRho produces 32 bytes" $ do
- let ss = SharedSecret (BS.replicate 32 0)
- DerivedKey rho = deriveRho ss
- BS.length rho @?= 32
- , testCase "deriveMu produces 32 bytes" $ do
- let ss = SharedSecret (BS.replicate 32 0)
- DerivedKey mu = deriveMu ss
- BS.length mu @?= 32
- , testCase "deriveUm produces 32 bytes" $ do
- let ss = SharedSecret (BS.replicate 32 0)
- DerivedKey um = deriveUm ss
- BS.length um @?= 32
- , testCase "derivePad produces 32 bytes" $ do
- let ss = SharedSecret (BS.replicate 32 0)
- DerivedKey pad = derivePad ss
- BS.length pad @?= 32
- , testCase "deriveAmmag produces 32 bytes" $ do
- let ss = SharedSecret (BS.replicate 32 0)
- DerivedKey ammag = deriveAmmag ss
- BS.length ammag @?= 32
- , testCase "different key types produce different results" $ do
- let ss = SharedSecret (BS.replicate 32 0x42)
- DerivedKey rho = deriveRho ss
- DerivedKey mu = deriveMu ss
- DerivedKey um = deriveUm ss
- assertBool "rho /= mu" (rho /= mu)
- assertBool "mu /= um" (mu /= um)
- assertBool "rho /= um" (rho /= um)
- ]
-
-testBlindPubKey :: TestTree
-testBlindPubKey = testGroup "key blinding" [
- testCase "blindPubKey produces valid key" $ do
- sk <- demand "roll32" $ Secp256k1.roll32 sessionKey
- pubKey <- demand "derive_pub" $ Secp256k1.derive_pub sk
- let bf = BlindingFactor (fromHex hop0BlindingFactorHex)
- case blindPubKey pubKey bf of
- Nothing ->
- assertFailure "blindPubKey returned Nothing"
- Just _blinded -> return ()
- , testCase "blindSecKey produces valid key" $ do
- let bf = BlindingFactor (fromHex hop0BlindingFactorHex)
- case blindSecKey sessionKey bf of
- Nothing ->
- assertFailure "blindSecKey returned Nothing"
- Just _blinded -> return ()
- ]
-
-testGenerateStream :: TestTree
-testGenerateStream = testGroup "generateStream" [
- testCase "produces correct length" $ do
- let dk = DerivedKey (BS.replicate 32 0)
- fmap BS.length (generateStream dk 100) @?= Just 100
- , testCase "1300-byte stream for hop_payloads" $ do
- let dk = DerivedKey (BS.replicate 32 0x42)
- fmap BS.length (generateStream dk 1300) @?= Just 1300
- , testCase "deterministic output" $ do
- let dk = DerivedKey (BS.replicate 32 0x55)
- stream1 = generateStream dk 64
- stream2 = generateStream dk 64
- stream1 @?= stream2
- , testCase "fails on a key that is not 32 bytes" $
- generateStream (DerivedKey "short") 16 @?= Nothing
- ]
-
-testHmacOperations :: TestTree
-testHmacOperations = testGroup "HMAC operations" [
- testCase "computeHmac produces 32 bytes" $ do
- let dk = DerivedKey (BS.replicate 32 0)
- hm = computeHmac dk "payloads" "assocdata"
- BS.length hm @?= 32
- , testCase "verifyHmac succeeds for matching" $ do
- let dk = DerivedKey (BS.replicate 32 0)
- hm = computeHmac dk "payloads" "assocdata"
- assertBool "verifyHmac should succeed"
- (verifyHmac hm hm)
- , testCase "verifyHmac fails for different" $ do
- let dk = DerivedKey (BS.replicate 32 0)
- hm1 = computeHmac dk "payloads1" "assocdata"
- hm2 = computeHmac dk "payloads2" "assocdata"
- assertBool "verifyHmac should fail"
- (not $ verifyHmac hm1 hm2)
- , testCase "verifyHmac fails for different lengths" $ do
- assertBool "verifyHmac should fail"
- (not $ verifyHmac "short" "different length")
- ]
-
--- Construct tests ----------------------------------------------------------
-
--- Test vectors from BOLT4 spec
-hop1PubKeyHex :: BS.ByteString
-hop1PubKeyHex =
- "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"
-
-hop2PubKeyHex :: BS.ByteString
-hop2PubKeyHex =
- "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"
-
-hop3PubKeyHex :: BS.ByteString
-hop3PubKeyHex =
- "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"
-
-hop4PubKeyHex :: BS.ByteString
-hop4PubKeyHex =
- "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"
-
--- Expected shared secrets from BOLT4 error test vectors
-hop1SharedSecretHex :: BS.ByteString
-hop1SharedSecretHex =
- "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae"
-
-hop2SharedSecretHex :: BS.ByteString
-hop2SharedSecretHex =
- "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc"
-
-hop3SharedSecretHex :: BS.ByteString
-hop3SharedSecretHex =
- "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d"
+demand :: String -> Maybe a -> IO a
+demand _ (Just a) = pure a
+demand msg Nothing = assertFailure msg
-hop4SharedSecretHex :: BS.ByteString
-hop4SharedSecretHex =
- "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328"
+right :: Show e => String -> Either e a -> IO a
+right _ (Right a) = pure a
+right msg (Left e) = assertFailure (msg ++ ": " ++ show e)
-constructTests :: TestTree
-constructTests = testGroup "packet construction" [
- testConstructErrorCases
- , testSharedSecretComputation
- , testPacketStructure
- , testSingleHop
- ]
+unhex :: BS.ByteString -> IO BS.ByteString
+unhex = demand "invalid hex" . B16.decode
-testConstructErrorCases :: TestTree
-testConstructErrorCases = testGroup "error cases" [
- testCase "rejects invalid session key (too short)" $ do
- let result = construct (BS.replicate 31 0x41) [] ""
- case result of
- Left InvalidSessionKey -> return ()
- _ -> assertFailure "Expected InvalidSessionKey"
- , testCase "rejects invalid session key (too long)" $ do
- let result = construct (BS.replicate 33 0x41) [] ""
- case result of
- Left InvalidSessionKey -> return ()
- _ -> assertFailure "Expected InvalidSessionKey"
- , testCase "rejects empty route" $ do
- let result = construct sessionKey [] ""
- case result of
- Left EmptyRoute -> return ()
- _ -> assertFailure "Expected EmptyRoute"
- , testCase "rejects too many hops" $ do
- pub <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- let emptyPayload = HopPayload Nothing Nothing Nothing
- Nothing Nothing Nothing []
- hop = Hop pub emptyPayload
- hops = replicate 21 hop
- result = construct sessionKey hops ""
- case result of
- Left TooManyHops -> return ()
- _ -> assertFailure "Expected TooManyHops"
- , testCase "rejects an empty hop payload" $ do
- pub <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- let emptyPayload = HopPayload Nothing Nothing Nothing
- Nothing Nothing Nothing []
- hops = [Hop pub minimalPayload, Hop pub emptyPayload]
- construct sessionKey hops "" @?= Left (InvalidHopPayload 1)
- ]
+hex_key :: BS.ByteString -> IO SecretKey
+hex_key h = unhex h >>= demand "invalid secret key" . secret_key
--- | A small, valid hop payload.
-minimalPayload :: HopPayload
-minimalPayload =
- HopPayload (Just 1000) (Just 500000) Nothing Nothing Nothing Nothing []
-
-testSharedSecretComputation :: TestTree
-testSharedSecretComputation =
- testCase "computes correct shared secrets (BOLT4 spec)" $ do
- pub0 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- pub1 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop1PubKeyHex)
- pub2 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop2PubKeyHex)
- pub3 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop3PubKeyHex)
- pub4 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop4PubKeyHex)
- let hops = [ Hop pub0 minimalPayload
- , Hop pub1 minimalPayload
- , Hop pub2 minimalPayload
- , Hop pub3 minimalPayload
- , Hop pub4 minimalPayload
- ]
- result = construct sessionKey hops ""
- case result of
- Left err ->
- assertFailure $ "construct failed: " ++ show err
- Right (_, secrets) -> case secrets of
- [SharedSecret ss0, SharedSecret ss1,
- SharedSecret ss2, SharedSecret ss3,
- SharedSecret ss4] -> do
- ss0 @?= fromHex hop0SharedSecretHex
- ss1 @?= fromHex hop1SharedSecretHex
- ss2 @?= fromHex hop2SharedSecretHex
- ss3 @?= fromHex hop3SharedSecretHex
- ss4 @?= fromHex hop4SharedSecretHex
- _ -> assertFailure "expected 5 shared secrets"
-
-testPacketStructure :: TestTree
-testPacketStructure =
- testCase "produces valid packet structure" $ do
- pub0 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- pub1 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop1PubKeyHex)
- let hops = [Hop pub0 minimalPayload,
- Hop pub1 minimalPayload]
- result = construct sessionKey hops ""
- case result of
- Left err ->
- assertFailure $ "construct failed: " ++ show err
- Right (packet, _) -> do
- opVersion packet @?= versionByte
- BS.length (opEphemeralKey packet) @?= pubkeySize
- BS.length (unHopPayloads (opHopPayloads packet))
- @?= hopPayloadsSize
- BS.length (unHmac32 (opHmac packet)) @?= hmacSize
- sk <- demand "roll32" $ Secp256k1.roll32 sessionKey
- expectedPub <- demand "derive_pub" $
- Secp256k1.derive_pub sk
- let expectedPubBytes =
- Secp256k1.serialize_point expectedPub
- opEphemeralKey packet @?= expectedPubBytes
-
-testSingleHop :: TestTree
-testSingleHop =
- testCase "constructs single-hop packet" $ do
- pub0 <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- let payload = HopPayload
- { hpAmtToForward = Just 1000
- , hpOutgoingCltv = Just 500000
- , hpShortChannelId = Nothing
- , hpPaymentData = Nothing
- , hpEncryptedData = Nothing
- , hpCurrentPathKey = Nothing
- , hpUnknownTlvs = []
- }
- hops = [Hop pub0 payload]
- result = construct sessionKey hops ""
- case result of
- Left err ->
- assertFailure $ "construct failed: " ++ show err
- Right (packet, secrets) -> do
- length secrets @?= 1
- -- Packet should be valid structure
- let encoded = encodeOnionPacket packet
- BS.length encoded @?= onionPacketSize
- -- Should decode back
- decoded <- demand "decodeOnionPacket" $
- decodeOnionPacket encoded
- decoded @?= packet
-
--- Process tests -----------------------------------------------------------
-
-processTests :: TestTree
-processTests = testGroup "packet processing" [
- testVersionValidation
- , testEphemeralKeyValidation
- , testHmacValidation
- , testProcessBasic
- , testPayloadLength
- ]
+hex_point :: BS.ByteString -> IO BOLT1.Point
+hex_point h = unhex h >>= demand "invalid point" . BOLT1.point
-testVersionValidation :: TestTree
-testVersionValidation = testGroup "version validation" [
- testCase "reject invalid version 0x01" $ do
- let packet = OnionPacket
- { opVersion = 0x01
- , opEphemeralKey = BS.replicate 33 0x02
- , opHopPayloads =
- unsafeHopPayloads (BS.replicate 1300 0x00)
- , opHmac =
- unsafeHmac32 (BS.replicate 32 0x00)
- }
- case process sessionKey packet BS.empty of
- Left (InvalidVersion v) -> v @?= 0x01
- Left other ->
- assertFailure $ "expected InvalidVersion, got: "
- ++ show other
- Right _ ->
- assertFailure "expected rejection, got success"
- , testCase "reject invalid version 0xFF" $ do
- let packet = OnionPacket
- { opVersion = 0xFF
- , opEphemeralKey = BS.replicate 33 0x02
- , opHopPayloads =
- unsafeHopPayloads (BS.replicate 1300 0x00)
- , opHmac =
- unsafeHmac32 (BS.replicate 32 0x00)
- }
- case process sessionKey packet BS.empty of
- Left (InvalidVersion v) -> v @?= 0xFF
- Left other ->
- assertFailure $ "expected InvalidVersion, got: "
- ++ show other
- Right _ ->
- assertFailure "expected rejection, got success"
- ]
+hex_secret :: BS.ByteString -> IO SharedSecret
+hex_secret h = unhex h >>= demand "invalid shared secret" . shared_secret
-testEphemeralKeyValidation :: TestTree
-testEphemeralKeyValidation =
- testGroup "ephemeral key validation" [
- testCase "reject invalid ephemeral key (all zeros)" $ do
- let packet = OnionPacket
- { opVersion = 0x00
- , opEphemeralKey = BS.replicate 33 0x00
- , opHopPayloads =
- unsafeHopPayloads (BS.replicate 1300 0x00)
- , opHmac =
- unsafeHmac32 (BS.replicate 32 0x00)
- }
- case process sessionKey packet BS.empty of
- Left InvalidEphemeralKey -> return ()
- Left other ->
- assertFailure $
- "expected InvalidEphemeralKey, got: "
- ++ show other
- Right _ ->
- assertFailure "expected rejection, got success"
- , testCase "reject malformed ephemeral key" $ do
- let packet = OnionPacket
- { opVersion = 0x00
- , opEphemeralKey =
- BS.pack (0x04 : replicate 32 0xAB)
- , opHopPayloads =
- unsafeHopPayloads (BS.replicate 1300 0x00)
- , opHmac =
- unsafeHmac32 (BS.replicate 32 0x00)
- }
- case process sessionKey packet BS.empty of
- Left InvalidEphemeralKey -> return ()
- Left other ->
- assertFailure $
- "expected InvalidEphemeralKey, got: "
- ++ show other
- Right _ ->
- assertFailure "expected rejection, got success"
- ]
+-- strip a BigSize length prefix, checking that it is exact
+unprefix :: BS.ByteString -> IO BS.ByteString
+unprefix bs = do
+ (len, body) <- demand "invalid length prefix" (BOLT1.decode_bigsize bs)
+ assertEqual "length prefix" (fromIntegral len) (BS.length body)
+ pure body
-testHmacValidation :: TestTree
-testHmacValidation = testGroup "HMAC validation" [
- testCase "reject invalid HMAC" $ do
- hop0PubKey <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- let ephKeyBytes =
- Secp256k1.serialize_point hop0PubKey
- packet = OnionPacket
- { opVersion = 0x00
- , opEphemeralKey = ephKeyBytes
- , opHopPayloads =
- unsafeHopPayloads (BS.replicate 1300 0x00)
- , opHmac =
- unsafeHmac32 (BS.replicate 32 0xFF)
- }
- case process sessionKey packet BS.empty of
- Left HmacMismatch -> return ()
- Left other ->
- assertFailure $ "expected HmacMismatch, got: "
- ++ show other
- Right _ ->
- assertFailure "expected rejection, got success"
+msat :: Word64 -> IO BOLT1.MilliSatoshi
+msat = demand "invalid amount" . BOLT1.milli_satoshi
+
+scid :: Word32 -> Word32 -> Word16 -> IO BOLT1.ShortChannelId
+scid b t o = demand "invalid scid" (BOLT1.short_channel_id b t o)
+
+tlvs :: [BOLT1.TlvRecord] -> IO BOLT1.TlvStream
+tlvs = demand "invalid tlv stream" . BOLT1.tlv_stream
+
+no_tlvs :: BOLT1.TlvStream
+no_tlvs = BOLT1.empty_tlv_stream
+
+key :: Word8 -> IO SecretKey
+key b = demand "invalid secret key" (secret_key (BS.replicate 32 b))
+
+ad :: BS.ByteString
+ad = BS.replicate 32 0x42
+
+-- a point with a valid prefix but not on the curve
+off_curve :: IO BOLT1.Point
+off_curve =
+ case [ p | x <- [0 .. 255 :: Word8]
+ , let bs = BS.cons 0x02 (BS.replicate 31 0 <> BS.singleton x)
+ , Secp256k1.parse_point bs == Nothing
+ , Just p <- [BOLT1.point bs] ] of
+ p : _ -> pure p
+ [] -> assertFailure "no off-curve point found"
+
+-- a payload with the given outgoing_cltv_value
+cltv_payload :: Word32 -> HopPayload
+cltv_payload c = empty_hop_payload { hp_outgoing_cltv_value = Just c }
+
+expect_process
+ :: ProcessError -> Either ProcessError ProcessResult -> Assertion
+expect_process e r = case r of
+ Left e' -> e' @?= e
+ Right _ -> assertFailure ("expected " ++ show e)
+
+forward :: Either ProcessError ProcessResult -> IO ForwardInfo
+forward r = case r of
+ Right (Forward f) -> pure f
+ Right (Receive _) -> assertFailure "expected Forward, got Receive"
+ Left e -> assertFailure ("process: " ++ show e)
+
+receive :: Either ProcessError ProcessResult -> IO ReceiveInfo
+receive r = case r of
+ Right (Receive i) -> pure i
+ Right (Forward _) -> assertFailure "expected Receive, got Forward"
+ Left e -> assertFailure ("process: " ++ show e)
+
+-- A single-hop onion, built independently of the library, whose
+-- decrypted hop_payloads is the given 1300-byte plaintext.
+raw_onion :: SecretKey -> BS.ByteString -> IO OnionPacket
+raw_onion node plain = do
+ let session = BS.replicate 32 0x77
+ e <- demand "session" (Secp256k1.parse_int256 session)
+ epub <- demand "session pub" (Secp256k1.derive_pub e)
+ n <- demand "node pub"
+ (Secp256k1.parse_point (BOLT1.un_point (public_key node)))
+ pt <- demand "ecdh" (Secp256k1.mul n e)
+ let ss = SHA256.hash (Secp256k1.serialize_point pt)
+ derive l = let SHA256.MAC k = SHA256.hmac l ss in k
+ stream <- right "chacha"
+ (ChaCha.cipher (derive "rho") 0 (BS.replicate 12 0)
+ (BS.replicate 1300 0))
+ let body = BS.packZipWith xor plain stream
+ SHA256.MAC mac = SHA256.hmac (derive "mu") (body <> ad)
+ pk <- demand "point" (BOLT1.point (Secp256k1.serialize_point epub))
+ OnionPacket 0 pk
+ <$> demand "hop_payloads" (hop_payloads body)
+ <*> demand "hmac" (hmac32 mac)
+
+-- pad a plaintext region to 1300 bytes
+pad1300 :: BS.ByteString -> BS.ByteString
+pad1300 bs = bs <> BS.replicate (1300 - BS.length bs) 0
+
+-- keys -----------------------------------------------------------------------
+
+key_tests :: TestTree
+key_tests = testGroup "keys" [
+ testCase "secret_key accepts 32-byte keys in range" $ do
+ _ <- key 0x01
+ sk <- demand "n - 1" . secret_key =<< unhex
+ "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140"
+ BS.length (BOLT1.un_point (public_key sk)) @?= 33
+ , testCase "secret_key rejects other lengths" $ do
+ assertNone (secret_key (BS.replicate 31 0x01))
+ assertNone (secret_key (BS.replicate 33 0x01))
+ assertNone (secret_key (BS.cons 0x00 (BS.replicate 32 0x01)))
+ assertNone (secret_key BS.empty)
+ , testCase "secret_key rejects zero and keys >= n" $ do
+ assertNone (secret_key (BS.replicate 32 0x00))
+ n <- unhex
+ "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
+ assertNone (secret_key n)
+ assertNone (secret_key (BS.replicate 32 0xff))
+ , testCase "public_key matches secp256k1" $ do
+ sk <- key 0x41
+ expected <- unhex V.onionPubKey0
+ BOLT1.un_point (public_key sk) @?= expected
+ , testCase "shared_secret requires 32 bytes" $ do
+ assertNone (shared_secret (BS.replicate 31 0))
+ assertNone (shared_secret (BS.replicate 33 0))
+ ss <- demand "ss" (shared_secret (BS.replicate 32 7))
+ un_shared_secret ss @?= BS.replicate 32 7
+ , testCase "shared secrets compare by value" $ do
+ a <- demand "a" (shared_secret (BS.replicate 32 7))
+ b <- demand "b" (shared_secret (BS.replicate 32 7))
+ c <- demand "c" (shared_secret (BS.cons 8 (BS.replicate 31 7)))
+ assertBool "equal" (a == b)
+ assertBool "not equal" (a /= c)
+ , testCase "secrets don't show their bytes" $ do
+ sk <- key 0x41
+ ss <- demand "ss" (shared_secret (BS.replicate 32 0x41))
+ ps <- demand "ps" (payment_secret (BS.replicate 32 0x41))
+ show sk @?= "SecretKey <redacted>"
+ show ss @?= "SharedSecret <redacted>"
+ show ps @?= "PaymentSecret <redacted>"
]
-
--- | Test basic packet processing with a properly constructed
--- packet.
-testProcessBasic :: TestTree
-testProcessBasic = testGroup "basic processing" [
- testCase "process valid packet (final hop)" $ do
- hop0PubKey <- demand "parse_point" $
- Secp256k1.parse_point (fromHex hop0PubKeyHex)
- let ephKeyBytes =
- Secp256k1.serialize_point hop0PubKey
- hopPayloadTlv = encodeHopPayload HopPayload
- { hpAmtToForward = Just 1000
- , hpOutgoingCltv = Just 500000
- , hpShortChannelId = Nothing
- , hpPaymentData = Nothing
- , hpEncryptedData = Nothing
- , hpCurrentPathKey = Nothing
- , hpUnknownTlvs = []
+ where
+ assertNone m = case m of
+ Nothing -> pure ()
+ Just _ -> assertFailure "expected Nothing"
+
+-- codecs ---------------------------------------------------------------------
+
+codec_tests :: TestTree
+codec_tests = testGroup "codecs" [
+ testCase "decode_onion_packet rejects other lengths" $ do
+ decode_onion_packet (BS.replicate 1365 0) @?= Left InvalidLength
+ decode_onion_packet (BS.replicate 1367 0) @?= Left InvalidLength
+ decode_onion_packet BS.empty @?= Left InvalidLength
+ , testCase "decode_onion_packet rejects a bad point prefix" $ do
+ let bs = BS.concat [ BS.singleton 0, BS.singleton 0x04
+ , BS.replicate 1364 0 ]
+ decode_onion_packet bs @?= Left InvalidPoint
+ , testCase "decode_onion_packet keeps the version byte" $ do
+ let bs = BS.concat [ BS.singleton 7, BS.singleton 0x02
+ , BS.replicate 1364 0 ]
+ pkt <- right "decode" (decode_onion_packet bs)
+ onion_version pkt @?= 7
+ encode_onion_packet pkt @?= bs
+ , testCase "smart constructors check lengths" $ do
+ hop_payloads (BS.replicate 1299 0) @?= Nothing
+ hop_payloads (BS.replicate 1301 0) @?= Nothing
+ hmac32 (BS.replicate 31 0) @?= Nothing
+ onion_hash (BS.replicate 33 0) @?= Nothing
+ case payment_secret (BS.replicate 31 0) of
+ Nothing -> pure ()
+ Just _ -> assertFailure "short payment secret"
+
+ , testCase "hop payload: all typed fields" $ do
+ amt <- msat 1000
+ tot <- msat 5000
+ s <- scid 1 2 3
+ ps <- demand "ps" (payment_secret (BS.replicate 32 0xaa))
+ pk <- hex_point V.onionPubKey0
+ ex <- tlvs [BOLT1.TlvRecord 19 "x"]
+ let hp = HopPayload
+ { hp_amt_to_forward = Just amt
+ , hp_outgoing_cltv_value = Just 500000
+ , hp_short_channel_id = Just s
+ , hp_payment_data = Just (PaymentData ps tot)
+ , hp_encrypted_data = Just "enc"
+ , hp_current_path_key = Just pk
+ , hp_payment_metadata = Just "meta"
+ , hp_total_amount_msat = Just tot
+ , hp_extra = ex
}
- payloadLen = BS.length hopPayloadTlv
- lenPrefix =
- encodeBigSize (fromIntegral payloadLen)
- payloadWithHmac = lenPrefix <> hopPayloadTlv
- <> BS.replicate 32 0x00
- padding = BS.replicate
- (1300 - BS.length payloadWithHmac) 0x00
- rawPayloads = payloadWithHmac <> padding
- ss <- demand "computeSharedSecret" $
- computeSharedSecret sessionKey hop0PubKey
- stream <- demand "generateStream" $
- generateStream (deriveRho ss) 1300
- let muKey = deriveMu ss
- encryptedPayloads =
- BS.pack (BS.zipWith xor rawPayloads stream)
- correctHmac =
- computeHmac muKey encryptedPayloads BS.empty
- packet = OnionPacket
- { opVersion = 0x00
- , opEphemeralKey = ephKeyBytes
- , opHopPayloads =
- unsafeHopPayloads encryptedPayloads
- , opHmac = unsafeHmac32 correctHmac
+ bs <- right "encode" (encode_hop_payload hp)
+ expected <- unhex $ BS.concat
+ [ "020203e8", "040307a120", "06080000010000020003"
+ , "0822", BS.concat (replicate 32 "aa"), "1388"
+ , "0a03656e63", "0c21", V.onionPubKey0, "10046d657461"
+ , "12021388", "130178" ]
+ bs @?= expected
+ decode_hop_payload bs @?= Right hp
+ , testCase "hop payload: zero values encode minimally" $ do
+ zero <- msat 0
+ let hp = empty_hop_payload { hp_amt_to_forward = Just zero
+ , hp_outgoing_cltv_value = Just 0 }
+ encode_hop_payload hp @?= Right "\STX\NUL\EOT\NUL"
+ decode_hop_payload "\STX\NUL\EOT\NUL" @?= Right hp
+ , testCase "hop payload: unknown odd types are kept" $ do
+ hp <- right "decode" (decode_hop_payload "\STX\SOH\SOH\ETX\NUL")
+ ex <- tlvs [BOLT1.TlvRecord 3 ""]
+ hp_extra hp @?= ex
+ encode_hop_payload hp @?= Right "\STX\SOH\SOH\ETX\NUL"
+ , testCase "hop payload: rejects unknown even types" $ do
+ decode_hop_payload "\DC4\NUL" @?=
+ Left (InvalidTlvStream (BOLT1.TlvUnknownEvenType 20))
+ decode_hop_payload "\SO\NUL" @?=
+ Left (InvalidTlvStream (BOLT1.TlvUnknownEvenType 14))
+ , testCase "hop payload: rejects malformed streams" $ do
+ decode_hop_payload "\EOT\SOH\SOH\STX\SOH\SOH" @?=
+ Left (InvalidTlvStream BOLT1.TlvNotStrictlyIncreasing)
+ decode_hop_payload "\STX\SOH\SOH\STX\SOH\SOH" @?=
+ Left (InvalidTlvStream BOLT1.TlvNotStrictlyIncreasing)
+ decode_hop_payload "\STX\ENQ\SOH" @?=
+ Left (InvalidTlvStream BOLT1.TlvTruncated)
+ decode_hop_payload "\STX\255\255\255\255\255\255\255\255\255" @?=
+ Left (InvalidTlvStream BOLT1.TlvTruncated)
+ decode_hop_payload "\253\NUL\STX\NUL" @?=
+ Left (InvalidTlvStream BOLT1.TlvNonMinimalBigSize)
+ , testCase "hop payload: rejects malformed values" $ do
+ -- non-minimal tu64 and tu32
+ decode_hop_payload "\STX\STX\NUL\SOH" @?= Left (InvalidTlvValue 2)
+ decode_hop_payload "\EOT\STX\NUL\SOH" @?= Left (InvalidTlvValue 4)
+ -- tu32 longer than 4 bytes
+ decode_hop_payload "\EOT\ENQ\SOH\SOH\SOH\SOH\SOH" @?=
+ Left (InvalidTlvValue 4)
+ -- amount above 21M BTC
+ decode_hop_payload "\STX\b\255\255\255\255\255\255\255\255" @?=
+ Left (InvalidTlvValue 2)
+ -- short_channel_id of 7 bytes
+ decode_hop_payload (BS.pack [6, 7] <> BS.replicate 7 0) @?=
+ Left (InvalidTlvValue 6)
+ -- payment_data with a 31-byte secret
+ decode_hop_payload (BS.pack [8, 31] <> BS.replicate 31 0) @?=
+ Left (InvalidTlvValue 8)
+ -- current_path_key of the wrong length or prefix
+ decode_hop_payload (BS.pack [12, 32] <> BS.replicate 32 2) @?=
+ Left (InvalidTlvValue 12)
+ decode_hop_payload (BS.pack [12, 33, 4] <> BS.replicate 32 2) @?=
+ Left (InvalidTlvValue 12)
+ -- total_amount_msat non-minimal
+ decode_hop_payload "\DC2\STX\NUL\SOH" @?= Left (InvalidTlvValue 18)
+ , testCase "hop payload: extra records may not have known types" $ do
+ ex <- tlvs [BOLT1.TlvRecord 16 "meta"]
+ encode_hop_payload empty_hop_payload { hp_extra = ex } @?=
+ Left ConflictingTlv
+ ex' <- tlvs [BOLT1.TlvRecord 2 "\SOH"]
+ encode_hop_payload (cltv_payload 1) { hp_extra = ex' } @?=
+ Left ConflictingTlv
+ , testCase "hop payload: extra records may have unknown even types" $ do
+ ex <- tlvs [BOLT1.TlvRecord 5482373484 (BS.replicate 32 1)]
+ bs <- right "encode" (encode_hop_payload (cltv_payload 1)
+ { hp_extra = ex })
+ decode_hop_payload bs @?=
+ Left (InvalidTlvStream (BOLT1.TlvUnknownEvenType 5482373484))
+
+ , testCase "blinded hop data: all typed fields" $ do
+ s <- scid 0 0 1729
+ pk <- hex_point V.rbDavePathKey
+ m <- msat 1500
+ ex <- tlvs [BOLT1.TlvRecord 561 "\DC24V"]
+ let d = BlindedHopData
+ { bhd_padding = Just (BS.replicate 3 0)
+ , bhd_short_channel_id = Just s
+ , bhd_next_node_id = Just pk
+ , bhd_path_id = Just "id"
+ , bhd_next_path_key_override = Just pk
+ , bhd_payment_relay = Just (PaymentRelay 36 150 10000)
+ , bhd_payment_constraints =
+ Just (PaymentConstraints 748005 m)
+ , bhd_allowed_features = Just (BOLT9.parse "\STX")
+ , bhd_extra = ex
}
-
- case process sessionKey packet BS.empty of
- Left err ->
- assertFailure $
- "expected success, got: " ++ show err
- Right (Receive ri) -> do
- hpAmtToForward (riPayload ri) @?= Just 1000
- hpOutgoingCltv (riPayload ri) @?= Just 500000
- Right (Forward _) ->
- assertFailure "expected Receive, got Forward"
- ]
-
--- | Encrypt 1300 bytes of plaintext hop_payloads into a packet for
--- the node with secret key 'sessionKey' (using its own public key
--- as the ephemeral key), with empty associated data.
-mkPacket :: BS.ByteString -> IO OnionPacket
-mkPacket plain = do
- pub <- demand "derive_pub"
- (Secp256k1.roll32 sessionKey >>= Secp256k1.derive_pub)
- ss <- demand "computeSharedSecret" (computeSharedSecret sessionKey pub)
- stream <- demand "generateStream" $
- generateStream (deriveRho ss) hopPayloadsSize
- let enc = BS.pack (BS.zipWith xor plain stream)
- mac = computeHmac (deriveMu ss) enc BS.empty
- hp <- demand "hopPayloads" (hopPayloads enc)
- hm <- demand "hmac32" (hmac32 mac)
- pure (OnionPacket versionByte (Secp256k1.serialize_point pub) hp hm)
-
--- | Pad (or truncate) plaintext hop_payloads to 1300 bytes.
-pad1300 :: BS.ByteString -> BS.ByteString
-pad1300 bs = BS.take hopPayloadsSize (bs <> BS.replicate hopPayloadsSize 0)
-
-assertInvalidPayload :: Either RejectReason ProcessResult -> Assertion
-assertInvalidPayload r = case r of
- Left (InvalidPayload _) -> pure ()
- Left e -> assertFailure ("expected InvalidPayload, got " ++ show e)
- Right _ -> assertFailure "expected rejection, got success"
-
-testPayloadLength :: TestTree
-testPayloadLength = testGroup "payload_length bounds" [
- testCase "rejects payload_length below 2" $ do
- pkt <- mkPacket (pad1300 (BS.pack [0x01, 0x02]))
- assertInvalidPayload (process sessionKey pkt BS.empty)
-
- , testCase "rejects payload_length past hop_payloads" $ do
- -- payload_length 2000, covered by an odd TLV (type 257, length
- -- 1994) that runs into the zero-extension stream
- let prefix = BS.pack [0xfd, 0x07, 0xd0, 0xfd, 0x01, 0x01,
- 0xfd, 0x07, 0xca]
- pkt <- mkPacket (pad1300 prefix)
- assertInvalidPayload (process sessionKey pkt BS.empty)
-
- , testCase "rejects payload_length of 2^64 - 1" $ do
- pkt <- mkPacket (pad1300 (BS.pack (0xff : replicate 8 0xff)))
- assertInvalidPayload (process sessionKey pkt BS.empty)
-
- , testCase "rejects payload one byte too long" $ do
- -- 3-byte prefix + 1266-byte payload + 32-byte HMAC = 1301
- let tlv = BS.pack [0x01, 0xfd, 0x04, 0xee] <> BS.replicate 1262 0
- pkt <- mkPacket (pad1300 (encodeBigSize 1266 <> tlv))
- assertInvalidPayload (process sessionKey pkt BS.empty)
-
- , testCase "accepts the largest payload that fits" $ do
- -- 3-byte prefix + 1265-byte payload + 32-byte HMAC = 1300
- let val = BS.replicate 1261 0
- tlv = BS.pack [0x01, 0xfd, 0x04, 0xed] <> val
- pkt <- mkPacket (pad1300 (encodeBigSize 1265 <> tlv))
- case process sessionKey pkt BS.empty of
- Right (Receive ri) ->
- hpUnknownTlvs (riPayload ri) @?= [TlvRecord 1 val]
- Right (Forward _) -> assertFailure "expected Receive"
- Left e -> assertFailure ("expected success, got " ++ show e)
- ]
-
--- Error tests -------------------------------------------------------------
-
-errorTests :: TestTree
-errorTests = testGroup "error handling" [
- testErrorConstruction
- , testErrorRoundtrip
- , testMultiHopWrapping
- , testErrorAttribution
- , testFailureMessageParsing
- , testFailureMessageFraming
- ]
-
--- Shared secrets for testing (deterministic)
-testSecret1 :: SharedSecret
-testSecret1 = SharedSecret (BS.replicate 32 0x11)
-
-testSecret2 :: SharedSecret
-testSecret2 = SharedSecret (BS.replicate 32 0x22)
-
-testSecret3 :: SharedSecret
-testSecret3 = SharedSecret (BS.replicate 32 0x33)
-
-testSecret4 :: SharedSecret
-testSecret4 = SharedSecret (BS.replicate 32 0x44)
-
--- Simple failure message for testing (htlc_msat 1000, height 800000)
-testFailure :: FailureMessage
-testFailure = FailureMessage IncorrectOrUnknownPaymentDetails
- (BS.pack [0, 0, 0, 0, 0, 0, 0x03, 0xe8, 0, 0x0c, 0x35, 0x00]) []
-
--- | Minimal well-formed data for a failure code.
-failureData :: FailureCode -> BS.ByteString
-failureData c
- | c `elem` [InvalidOnionVersion, InvalidOnionHmac, InvalidOnionKey] =
- BS.replicate 32 0xab
- | c `elem` [TemporaryChannelFailure, ExpiryTooSoon] = BS.pack [0, 0]
- | c `elem` [AmountBelowMinimum, FeeInsufficient] =
- BS.replicate 8 0x01 <> BS.pack [0, 0]
- | c == IncorrectCltvExpiry = BS.replicate 4 0x01 <> BS.pack [0, 0]
- | c == ChannelDisabled = BS.pack [0, 1, 0, 0]
- | c == IncorrectOrUnknownPaymentDetails = BS.replicate 12 0x01
- | c == FinalIncorrectCltvExpiry = BS.replicate 4 0x01
- | c == FinalIncorrectHtlcAmount = BS.replicate 8 0x01
- | c == InvalidOnionPayload = BS.pack [0x02, 0x00, 0x05]
- | otherwise = BS.empty
-
--- | Read a big-endian u16 at an offset, failing the test if short.
-u16At :: Int -> BS.ByteString -> IO Int
-u16At off bs = case BS.unpack (BS.take 2 (BS.drop off bs)) of
- [hi, lo] -> pure (fromIntegral hi * 256 + fromIntegral lo)
- _ -> assertFailure "u16At: input too short"
-
--- | 'constructError', failing the test on Nothing.
-mkError :: SharedSecret -> FailureMessage -> IO ErrorPacket
-mkError ss fm = demand "constructError" (constructError ss fm)
-
--- | 'wrapError', failing the test on Nothing.
-wrapErr :: SharedSecret -> ErrorPacket -> IO ErrorPacket
-wrapErr ss pkt = demand "wrapError" (wrapError ss pkt)
-
--- | Strip the failing node's obfuscation from a return packet and
--- return (failure_len, pad_len, packet length).
-returnPacketLengths :: SharedSecret -> ErrorPacket -> IO (Int, Int, Int)
-returnPacketLengths ss pkt = do
- ErrorPacket raw <- wrapErr ss pkt
- failureLen <- u16At 32 raw
- padLen <- u16At (34 + failureLen) raw
- pure (failureLen, padLen, BS.length raw)
-
-testErrorConstruction :: TestTree
-testErrorConstruction = testGroup "error packet construction" [
- testCase "short message is padded to 256" $ do
- let failure = FailureMessage TemporaryNodeFailure BS.empty []
- errPacket <- mkError testSecret1 failure
- (fl, pl, size) <- returnPacketLengths testSecret1 errPacket
- fl @?= 2
- fl + pl @?= minErrorPacketSize
- size @?= 32 + 2 + fl + 2 + pl
- , testCase "long message is not truncated or padded" $ do
- let failure = FailureMessage TemporaryNodeFailure BS.empty
- [TlvRecord 1 (BS.replicate 300 0x00)]
- errPacket <- mkError testSecret1 failure
- (fl, pl, size) <- returnPacketLengths testSecret1 errPacket
- fl @?= 2 + 1 + 3 + 300
- pl @?= 0
- size @?= 32 + 2 + fl + 2
- ]
-
-testErrorRoundtrip :: TestTree
-testErrorRoundtrip =
- testCase "construct and unwrap roundtrip" $ do
- errPacket <- mkError testSecret1 testFailure
- let result = unwrapError [testSecret1] errPacket
- case result of
- Attributed idx msg -> do
- idx @?= 0
- fmCode msg @?= IncorrectOrUnknownPaymentDetails
- UnknownOrigin _ ->
- assertFailure
- "Expected Attributed, got UnknownOrigin"
-
-testMultiHopWrapping :: TestTree
-testMultiHopWrapping = testGroup "multi-hop wrapping" [
- testCase "3-hop route, error from hop 2 (final)" $ do
- let secrets =
- [testSecret1, testSecret2, testSecret3]
- err0 <- mkError testSecret3 testFailure
- err1 <- wrapErr testSecret2 err0
- err2 <- wrapErr testSecret1 err1
- let result = unwrapError secrets err2
- case result of
- Attributed idx msg -> do
- idx @?= 2
- fmCode msg @?= IncorrectOrUnknownPaymentDetails
- UnknownOrigin _ ->
- assertFailure
- "Expected Attributed, got UnknownOrigin"
-
- , testCase "4-hop route, error from hop 1" $ do
- let secrets = [testSecret1, testSecret2,
- testSecret3, testSecret4]
- err0 <- mkError testSecret2 testFailure
- err1 <- wrapErr testSecret1 err0
- let result = unwrapError secrets err1
- case result of
- Attributed idx msg -> do
- idx @?= 1
- fmCode msg @?= IncorrectOrUnknownPaymentDetails
- UnknownOrigin _ ->
- assertFailure
- "Expected Attributed, got UnknownOrigin"
-
- , testCase "4-hop route, error from hop 0 (first)" $ do
- let secrets = [testSecret1, testSecret2,
- testSecret3, testSecret4]
- err0 <- mkError testSecret1 testFailure
- let result = unwrapError secrets err0
- case result of
- Attributed idx msg -> do
- idx @?= 0
- fmCode msg @?= IncorrectOrUnknownPaymentDetails
- UnknownOrigin _ ->
- assertFailure
- "Expected Attributed, got UnknownOrigin"
+ bs <- right "encode" (encode_blinded_hop_data d)
+ expected <- unhex $ BS.concat
+ [ "0103000000", "020800000000000006c1", "0421", V.rbDavePathKey
+ , "06026964", "0821", V.rbDavePathKey, "0a080024000000962710"
+ , "0c06000b69e505dc", "0e0102", "fd023103123456" ]
+ bs @?= expected
+ decode_blinded_hop_data bs @?= Right d
+ , testCase "blinded hop data: unknown types" $ do
+ decode_blinded_hop_data "\DLE\NUL" @?=
+ Left (InvalidTlvStream (BOLT1.TlvUnknownEvenType 16))
+ d <- right "decode" (decode_blinded_hop_data "\ETX\NUL")
+ ex <- tlvs [BOLT1.TlvRecord 3 ""]
+ bhd_extra d @?= ex
+ , testCase "blinded hop data: rejects malformed values" $ do
+ -- payment_relay shorter than its fixed fields
+ decode_blinded_hop_data "\n\ENQ\NUL\NUL\NUL\NUL\NUL" @?=
+ Left (InvalidTlvValue 10)
+ -- payment_relay with a non-minimal fee_base_msat
+ decode_blinded_hop_data "\n\a\NUL\NUL\NUL\NUL\NUL\NUL\NUL" @?=
+ Left (InvalidTlvValue 10)
+ -- payment_constraints shorter than its fixed fields
+ decode_blinded_hop_data "\f\ETX\NUL\NUL\NUL" @?=
+ Left (InvalidTlvValue 12)
+ -- next_node_id and next_path_key_override of the wrong length
+ decode_blinded_hop_data (BS.pack [4, 2, 2, 2]) @?=
+ Left (InvalidTlvValue 4)
+ decode_blinded_hop_data (BS.pack [8, 2, 2, 2]) @?=
+ Left (InvalidTlvValue 8)
+ -- short_channel_id of 9 bytes
+ decode_blinded_hop_data (BS.pack [2, 9] <> BS.replicate 9 0) @?=
+ Left (InvalidTlvValue 2)
+ , testCase "blinded hop data: extra records may not have known types" $ do
+ ex <- tlvs [BOLT1.TlvRecord 1 ""]
+ encode_blinded_hop_data empty_blinded_hop_data { bhd_extra = ex } @?=
+ Left ConflictingTlv
]
-testErrorAttribution :: TestTree
-testErrorAttribution = testGroup "error attribution" [
- testCase "wrong secrets gives UnknownOrigin" $ do
- err <- mkError testSecret1 testFailure
- let wrongSecrets = [testSecret2, testSecret3]
- result = unwrapError wrongSecrets err
- case result of
- UnknownOrigin _ -> return ()
- Attributed _ _ ->
- assertFailure
- "Expected UnknownOrigin with wrong secrets"
-
- , testCase "empty secrets gives UnknownOrigin" $ do
- err <- mkError testSecret1 testFailure
- let result = unwrapError [] err
- case result of
- UnknownOrigin _ -> return ()
- Attributed _ _ ->
- assertFailure
- "Expected UnknownOrigin with empty secrets"
-
- , testCase "correct attribution with multiple failures" $ do
- let failures =
- [ (TemporaryNodeFailure, testSecret1)
- , (PermanentNodeFailure, testSecret2)
- , (InvalidOnionHmac, testSecret3)
+-- failure messages -----------------------------------------------------------
+
+failure_tests :: TestTree
+failure_tests = testGroup "failure messages" [
+ testCase "encodings and codes of every failure" $ do
+ h <- demand "hash" (onion_hash (BS.replicate 32 0xab))
+ m <- msat 100
+ let hh = BS.concat (replicate 32 "ab")
+ cases =
+ [ (TemporaryNodeFailure, "2002")
+ , (PermanentNodeFailure, "6002")
+ , (RequiredNodeFeatureMissing, "6003")
+ , (InvalidOnionVersion h, "c004" <> hh)
+ , (InvalidOnionHmac h, "c005" <> hh)
+ , (InvalidOnionKey h, "c006" <> hh)
+ , (TemporaryChannelFailure "cu", "100700026375")
+ , (PermanentChannelFailure, "4008")
+ , (RequiredChannelFeatureMissing, "4009")
+ , (UnknownNextPeer, "400a")
+ , (AmountBelowMinimum m "", "100b00000000000000640000")
+ , (FeeInsufficient m "cu", "100c000000000000006400026375")
+ , (IncorrectCltvExpiry 800000 "", "100d000c35000000")
+ , (ExpiryTooSoon "", "100e0000")
+ , ( IncorrectOrUnknownPaymentDetails m 800000
+ , "400f0000000000000064000c3500" )
+ , (FinalIncorrectCltvExpiry 800000, "0012000c3500")
+ , (FinalIncorrectHtlcAmount m, "00130000000000000064")
+ , (ChannelDisabled 0 "", "101400000000")
+ , (ExpiryTooFar, "0015")
+ , (InvalidOnionPayload Nothing, "4016")
+ , (InvalidOnionPayload (Just (2, 7)), "401602 0007")
+ , (MppTimeout, "0017")
+ , (InvalidOnionBlinding h, "c018" <> hh)
+ , (UnknownFailure 0x0042 "data", "004264617461")
]
- mapM_ (\(code, secret) -> do
- let failure = FailureMessage code (failureData code) []
- err <- mkError secret failure
- let result = unwrapError [secret] err
- case result of
- Attributed 0 msg -> fmCode msg @?= code
- _ -> assertFailure $
- "Failed for code: " ++ show code
- ) failures
- ]
-
-testFailureMessageParsing :: TestTree
-testFailureMessageParsing =
- testGroup "failure message parsing" [
- testCase "code with data" $ do
- let failData = BS.pack [0, 0, 0, 0, 0, 0, 0x03, 0xe8, 0, 0]
- failure =
- FailureMessage AmountBelowMinimum failData []
- err <- mkError testSecret1 failure
- let result = unwrapError [testSecret1] err
- case result of
- Attributed 0 msg -> do
- fmCode msg @?= AmountBelowMinimum
- fmData msg @?= failData
- _ -> assertFailure "Expected Attributed"
-
- , testCase "various failure codes roundtrip" $ do
- let codes =
- [ InvalidRealm
- , TemporaryNodeFailure
- , PermanentNodeFailure
- , InvalidOnionHmac
- , TemporaryChannelFailure
- , IncorrectOrUnknownPaymentDetails
- ]
- mapM_ (\code -> do
- let failure = FailureMessage code (failureData code) []
- err <- mkError testSecret1 failure
- let result = unwrapError [testSecret1] err
- case result of
- Attributed 0 msg -> fmCode msg @?= code
- _ -> assertFailure $
- "Failed for code: " ++ show code
- ) codes
- ]
-
-testFailureMessageFraming :: TestTree
-testFailureMessageFraming = testGroup "failure message framing" [
- testCase "no length after the failure code" $ do
- encodeFailureMessage
- (FailureMessage TemporaryNodeFailure BS.empty [])
- @?= BS.pack [0x20, 0x02]
- encodeFailureMessage testFailure
- @?= BS.pack [ 0x40, 0x0f, 0, 0, 0, 0, 0, 0, 0x03, 0xe8
- , 0, 0x0c, 0x35, 0x00 ]
- , testCase "decodes spec-framed messages" $ do
- decodeFailureMessage (BS.pack [0x20, 0x02])
- @?= Just (FailureMessage TemporaryNodeFailure BS.empty [])
- decodeFailureMessage (encodeFailureMessage testFailure)
- @?= Just testFailure
- , testCase "splits data from the TLV stream by code" $ do
- let fm = FailureMessage FinalIncorrectHtlcAmount
- (BS.replicate 8 0x07) [TlvRecord 3 (BS.pack [1, 2])]
- decodeFailureMessage (encodeFailureMessage fm) @?= Just fm
- , testCase "channel_update length is honoured" $ do
- let fm = FailureMessage TemporaryChannelFailure
- (BS.pack [0, 3, 0xaa, 0xbb, 0xcc]) []
- decodeFailureMessage (encodeFailureMessage fm) @?= Just fm
- , testCase "ignores extra bytes" $
- decodeFailureMessage (BS.pack [0x20, 0x02, 0xff])
- @?= Just (FailureMessage TemporaryNodeFailure BS.empty [])
- , testCase "unknown code keeps the remainder as data" $
- decodeFailureMessage (BS.pack [0x20, 0x63, 0x01, 0x02])
- @?= Just (FailureMessage (FailureCode 0x2063)
- (BS.pack [0x01, 0x02]) [])
- , testCase "rejects truncated data" $ do
- decodeFailureMessage (BS.pack [0x40, 0x0f, 0, 0, 0]) @?= Nothing
- decodeFailureMessage (BS.pack [0x10, 0x07, 0, 2, 0xaa]) @?= Nothing
- decodeFailureMessage (BS.pack [0x20]) @?= Nothing
+ forM_ cases $ \(f, hx) -> do
+ bs <- unhex (BS.filter (/= 0x20) hx)
+ let fm = FailureMessage f no_tlvs
+ encode_failure_message fm @?= Right bs
+ decode_failure_message bs @?= Right fm
+ Just (failure_code f) @?= fmap fst (BOLT1.decode_u16 bs)
+ , testCase "flags" $ do
+ h <- demand "hash" (onion_hash (BS.replicate 32 0))
+ let flags f = (is_badonion f, is_perm f, is_node f, is_update f)
+ flags TemporaryNodeFailure @?= (False, False, True, False)
+ flags PermanentNodeFailure @?= (False, True, True, False)
+ flags (InvalidOnionBlinding h) @?= (True, True, False, False)
+ flags (ExpiryTooSoon "") @?= (False, False, False, True)
+ flags MppTimeout @?= (False, False, False, False)
+ flags (UnknownFailure 0xf000 "") @?= (True, True, True, True)
+ , testCase "spec example: incorrect_or_unknown_payment_details" $ do
+ bs <- unhex "400f0000000000000064000c3500"
+ m <- msat 100
+ decode_failure_message bs @?= Right
+ (FailureMessage (IncorrectOrUnknownPaymentDetails m 800000) no_tlvs)
+ , testCase "a trailing TLV stream is kept" $ do
+ ex <- tlvs [BOLT1.TlvRecord 34001 (BS.replicate 3 0x80)]
+ let fm = FailureMessage TemporaryNodeFailure ex
+ bs <- right "encode" (encode_failure_message fm)
+ bs @?= "\x20\x02\xfd\x84\xd1\x03\x80\x80\x80"
+ decode_failure_message bs @?= Right fm
+ , testCase "other trailing bytes are ignored" $ do
+ let ignored f bs = decode_failure_message bs @?=
+ Right (FailureMessage f no_tlvs)
+ ignored TemporaryNodeFailure "\x20\x02\xff"
+ -- an even type: not a valid stream without known types
+ ignored TemporaryNodeFailure "\x20\x02\x02\x00"
+ ignored (FinalIncorrectCltvExpiry 1) "\NUL\DC2\NUL\NUL\NUL\SOH\SOH"
+ , testCase "truncated data is rejected" $ do
+ decode_failure_message "" @?= Left InvalidLength
+ decode_failure_message "\x20" @?= Left InvalidLength
+ decode_failure_message "\xc0\x05\x00" @?=
+ Left (InvalidFailureData 0xc005)
+ decode_failure_message "\x10\x07\x00\x02\x00" @?=
+ Left (InvalidFailureData 0x1007)
+ decode_failure_message "\x40\x0f\x00\x00\x00\x00\x00\x00\x00\x64" @?=
+ Left (InvalidFailureData 0x400f)
+ decode_failure_message "\x00\x13\x00" @?=
+ Left (InvalidFailureData 0x0013)
+ decode_failure_message "\x10\x14\x00\x00\x00" @?=
+ Left (InvalidFailureData 0x1014)
+ , testCase "amounts above 21M BTC are rejected" $
+ decode_failure_message "\x00\x13\xff\xff\xff\xff\xff\xff\xff\xff" @?=
+ Left (InvalidFailureData 0x0013)
+ , testCase "unknown codes keep their data" $
+ decode_failure_message "\x12\x34\x01\x00" @?=
+ Right (FailureMessage (UnknownFailure 0x1234 "\x01\x00") no_tlvs)
+ , testCase "channel_update longer than 65535 bytes" $
+ encode_failure_message
+ (FailureMessage (ExpiryTooSoon (BS.replicate 65536 0)) no_tlvs)
+ @?= Left FieldTooLong
]
--- Blinding tests -----------------------------------------------------------
-
--- Test data setup
-
-testSeed :: BS.ByteString
-testSeed = BS.pack [1..32]
-
-makeSecKey :: Word8 -> BS.ByteString
-makeSecKey seed = BS.pack $ replicate 31 0x00 ++ [seed]
-
-makePubKey :: Word8 -> Maybe Secp256k1.Projective
-makePubKey seed = do
- sk <- Secp256k1.roll32 (makeSecKey seed)
- Secp256k1.derive_pub sk
-
-testNodeSecKey1, testNodeSecKey2,
- testNodeSecKey3 :: BS.ByteString
-testNodeSecKey1 = makeSecKey 0x11
-testNodeSecKey2 = makeSecKey 0x22
-testNodeSecKey3 = makeSecKey 0x33
-
-testNodePubKey1, testNodePubKey2,
- testNodePubKey3 :: Secp256k1.Projective
-testNodePubKey1 = case makePubKey 0x11 of
- Just pk -> pk
- Nothing -> error "testNodePubKey1: invalid key"
-testNodePubKey2 = case makePubKey 0x22 of
- Just pk -> pk
- Nothing -> error "testNodePubKey2: invalid key"
-testNodePubKey3 = case makePubKey 0x33 of
- Just pk -> pk
- Nothing -> error "testNodePubKey3: invalid key"
-
-testSharedSecretBS :: SharedSecret
-testSharedSecretBS = SharedSecret (BS.pack [0x42..0x61])
-
-emptyHopData :: BlindedHopData
-emptyHopData = BlindedHopData
- Nothing Nothing Nothing Nothing
- Nothing Nothing Nothing Nothing
-
-sampleHopData :: BlindedHopData
-sampleHopData = BlindedHopData
- { bhdPadding = Nothing
- , bhdShortChannelId = Just (mkScid 700000 1234 0)
- , bhdNextNodeId = Nothing
- , bhdPathId = Just (BS.pack [0x42, 0x42])
- , bhdNextPathKeyOverride = Nothing
- , bhdPaymentRelay = Just (PaymentRelay 40 1000 500)
- , bhdPaymentConstraints =
- Just (PaymentConstraints 144 1000000)
- , bhdAllowedFeatures = Nothing
- }
-
-hopDataWithNextNode :: BlindedHopData
-hopDataWithNextNode = emptyHopData
- { bhdNextNodeId =
- Just (Secp256k1.serialize_point testNodePubKey2)
- }
-
--- 1. Key Derivation Tests -------------------------------------------------
-
-blindingKeyDerivationTests :: TestTree
-blindingKeyDerivationTests = testGroup "key derivation" [
- testCase "deriveBlindingRho produces 32 bytes" $ do
- let DerivedKey rho =
- deriveBlindingRho testSharedSecretBS
- BS.length rho @?= 32
-
- , testCase "deriveBlindingRho is deterministic" $ do
- let rho1 = deriveBlindingRho testSharedSecretBS
- rho2 = deriveBlindingRho testSharedSecretBS
- rho1 @?= rho2
-
- , testCase "deriveBlindingRho differs for diff secrets" $ do
- let ss1 = SharedSecret (BS.replicate 32 0x00)
- ss2 = SharedSecret (BS.replicate 32 0x01)
- rho1 = deriveBlindingRho ss1
- rho2 = deriveBlindingRho ss2
- assertBool "rho values should differ" (rho1 /= rho2)
-
- , testCase "deriveBlindedNodeId produces 33 bytes" $ do
- case deriveBlindedNodeId
- testSharedSecretBS testNodePubKey1 of
- Nothing ->
- assertFailure
- "deriveBlindedNodeId returned Nothing"
- Just blindedId -> BS.length blindedId @?= 33
-
- , testCase "deriveBlindedNodeId is deterministic" $ do
- let result1 = deriveBlindedNodeId
- testSharedSecretBS testNodePubKey1
- result2 = deriveBlindedNodeId
- testSharedSecretBS testNodePubKey1
- result1 @?= result2
-
- , testCase "deriveBlindedNodeId differs for diff nodes" $ do
- let result1 = deriveBlindedNodeId
- testSharedSecretBS testNodePubKey1
- result2 = deriveBlindedNodeId
- testSharedSecretBS testNodePubKey2
- assertBool "blinded node IDs should differ"
- (result1 /= result2)
- ]
-
--- 2. Ephemeral Key Iteration Tests ----------------------------------------
-
--- | Derive the public key for testSeed
-testSeedPubKey :: Secp256k1.Projective
-testSeedPubKey = case Secp256k1.roll32 testSeed of
- Nothing -> error "testSeedPubKey: invalid seed"
- Just sk -> case Secp256k1.derive_pub sk of
- Nothing -> error "testSeedPubKey: invalid key"
- Just pk -> pk
-
-blindingEphemeralKeyTests :: TestTree
-blindingEphemeralKeyTests =
- testGroup "ephemeral key iteration" [
- testCase "nextEphemeral produces valid keys" $ do
- case nextEphemeral
- testSeed testSeedPubKey testSharedSecretBS of
- Nothing ->
- assertFailure "nextEphemeral returned Nothing"
- Just (newSecKey, newPubKey) -> do
- BS.length newSecKey @?= 32
- let serialized =
- Secp256k1.serialize_point newPubKey
- BS.length serialized @?= 33
-
- , testCase "nextEphemeral: sec key derives pub key" $ do
- case nextEphemeral
- testSeed testSeedPubKey testSharedSecretBS of
- Nothing ->
- assertFailure "nextEphemeral returned Nothing"
- Just (newSecKey, newPubKey) -> do
- sk <- demand "roll32" $
- Secp256k1.roll32 newSecKey
- derivedPub <- demand "derive_pub" $
- Secp256k1.derive_pub sk
- derivedPub @?= newPubKey
-
- , testCase "nextEphemeral is deterministic" $ do
- let result1 = nextEphemeral
- testSeed testSeedPubKey
- testSharedSecretBS
- result2 = nextEphemeral
- testSeed testSeedPubKey
- testSharedSecretBS
- result1 @?= result2
-
- , testCase "nextEphemeral differs for diff secrets" $ do
- let ss1 = SharedSecret (BS.replicate 32 0xAA)
- ss2 = SharedSecret (BS.replicate 32 0xBB)
- result1 = nextEphemeral
- testSeed testSeedPubKey ss1
- result2 = nextEphemeral
- testSeed testSeedPubKey ss2
- assertBool "results should differ"
- (result1 /= result2)
-
- , testCase "nextEphemeral rejects a secret not of 32 bytes" $ do
- nextEphemeral (BS.singleton 0x07) testSeedPubKey testSharedSecretBS
- @?= Nothing
- nextEphemeral (BS.cons 0x00 testSeed) testSeedPubKey
- testSharedSecretBS @?= Nothing
- ]
-
--- 3. TLV Encoding/Decoding Tests -----------------------------------------
-
-blindingTlvTests :: TestTree
-blindingTlvTests = testGroup "TLV encoding/decoding" [
- testCase "roundtrip: empty hop data" $ do
- let encoded = encodeBlindedHopData emptyHopData
- decoded = decodeBlindedHopData encoded
- decoded @?= Just emptyHopData
-
- , testCase "roundtrip: sample hop data" $ do
- let encoded = encodeBlindedHopData sampleHopData
- decoded = decodeBlindedHopData encoded
- decoded @?= Just sampleHopData
-
- , testCase "roundtrip: hop data with next node ID" $ do
- let encoded = encodeBlindedHopData hopDataWithNextNode
- decoded = decodeBlindedHopData encoded
- decoded @?= Just hopDataWithNextNode
-
- , testCase "roundtrip: hop data with padding" $ do
- let hd = emptyHopData
- { bhdPadding = Just (BS.replicate 16 0x00) }
- encoded = encodeBlindedHopData hd
- decoded = decodeBlindedHopData encoded
- decoded @?= Just hd
-
- , testCase "PaymentRelay encoding/decoding" $ do
- let relay = PaymentRelay 40 1000 500
- hd = emptyHopData
- { bhdPaymentRelay = Just relay }
- encoded = encodeBlindedHopData hd
- decoded = decodeBlindedHopData encoded
- case decoded of
- Nothing ->
- assertFailure
- "decodeBlindedHopData returned Nothing"
- Just d -> bhdPaymentRelay d @?= Just relay
-
- , testCase "PaymentConstraints encoding/decoding" $ do
- let constraints = PaymentConstraints 144 1000000
- hd = emptyHopData
- { bhdPaymentConstraints = Just constraints }
- encoded = encodeBlindedHopData hd
- decoded = decodeBlindedHopData encoded
- case decoded of
- Nothing ->
- assertFailure
- "decodeBlindedHopData returned Nothing"
- Just d ->
- bhdPaymentConstraints d @?= Just constraints
-
- , testCase "decode empty bytestring" $ do
- let decoded = decodeBlindedHopData BS.empty
- decoded @?= Just emptyHopData
- ]
-
--- 4. Encryption/Decryption Tests ------------------------------------------
-
-blindingEncryptionTests :: TestTree
-blindingEncryptionTests =
- testGroup "encryption/decryption" [
- testCase "roundtrip: encrypt then decrypt" $ do
- let rho = deriveBlindingRho testSharedSecretBS
- encrypted <- demand "encryptHopData" $
- encryptHopData rho sampleHopData
- decryptHopData rho encrypted @?= Just sampleHopData
-
- , testCase "roundtrip: empty hop data" $ do
- let rho = deriveBlindingRho testSharedSecretBS
- encrypted <- demand "encryptHopData" $
- encryptHopData rho emptyHopData
- decryptHopData rho encrypted @?= Just emptyHopData
-
- , testCase "decryption with wrong key fails" $ do
- let rho1 = deriveBlindingRho testSharedSecretBS
- rho2 = deriveBlindingRho
- (SharedSecret (BS.replicate 32 0xFF))
- encrypted <- demand "encryptHopData" $
- encryptHopData rho1 sampleHopData
- assertBool "decryption should fail or produce garbage"
- (decryptHopData rho2 encrypted /= Just sampleHopData)
-
- , testCase "encrypt is deterministic" $ do
- let rho = deriveBlindingRho testSharedSecretBS
- encrypted1 = encryptHopData rho sampleHopData
- encrypted2 = encryptHopData rho sampleHopData
- encrypted1 @?= encrypted2
-
- , testCase "encrypt fails on a key that is not 32 bytes" $
- encryptHopData (DerivedKey "short") sampleHopData @?= Nothing
+-- construct ------------------------------------------------------------------
+
+construct_tests :: TestTree
+construct_tests = testGroup "construct" [
+ testCase "empty route" $ do
+ sk <- key 0x41
+ assertConstructError EmptyRoute (construct sk [] ad)
+ , testCase "more than 20 hops" $ do
+ sk <- key 0x41
+ node <- key 0x42
+ let hop = Hop (public_key node) (cltv_payload 1)
+ assertConstructError TooManyHops (construct sk (replicate 21 hop) ad)
+ case construct sk (replicate 20 hop) ad of
+ Right (_, sss) -> length sss @?= 20
+ Left e -> assertFailure (show e)
+ , testCase "invalid hop public key" $ do
+ sk <- key 0x41
+ node <- key 0x42
+ bad <- off_curve
+ let good = Hop (public_key node) (cltv_payload 1)
+ assertConstructError (InvalidHopPubKey 1)
+ (construct sk [good, Hop bad (cltv_payload 1), good] ad)
+ , testCase "payloads under 2 bytes" $ do
+ sk <- key 0x41
+ node <- key 0x42
+ let good = Hop (public_key node) (cltv_payload 1)
+ assertConstructError (InvalidHopPayload 1)
+ (construct sk [good, Hop (public_key node) empty_hop_payload] ad)
+ , testCase "unencodable payload" $ do
+ sk <- key 0x41
+ node <- key 0x42
+ ex <- tlvs [BOLT1.TlvRecord 4 "\SOH"]
+ let bad = Hop (public_key node) empty_hop_payload { hp_extra = ex }
+ assertConstructError (InvalidHopPayload 0) (construct sk [bad] ad)
+ , testCase "payloads over 1300 bytes" $ do
+ sk <- key 0x41
+ node <- key 0x42
+ -- a 1265-byte payload exactly fills hop_payloads
+ ex <- tlvs [BOLT1.TlvRecord 1 (BS.replicate 1261 0)]
+ let full = Hop (public_key node) empty_hop_payload { hp_extra = ex }
+ case construct sk [full] ad of
+ Right _ -> pure ()
+ Left e -> assertFailure (show e)
+ ex' <- tlvs [BOLT1.TlvRecord 1 (BS.replicate 1262 0)]
+ let over = Hop (public_key node) empty_hop_payload { hp_extra = ex' }
+ assertConstructError (PayloadsTooLarge 1301) (construct sk [over] ad)
+ let hops = replicate 2 (Hop (public_key node) (cltv_payload 1))
+ assertConstructError (PayloadsTooLarge 1372)
+ (construct sk (full : hops) ad)
]
-
--- 5. createBlindedPath Tests ----------------------------------------------
-
-blindingCreatePathTests :: TestTree
-blindingCreatePathTests = testGroup "createBlindedPath" [
- testCase "create path with 2 hops" $ do
- let nodes = [(testNodePubKey1, emptyHopData),
- (testNodePubKey2, sampleHopData)]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> do
- length (bpBlindedHops path) @?= 2
- let serialized =
- Secp256k1.serialize_point
- (bpBlindingKey path)
- BS.length serialized @?= 33
-
- , testCase "create path with 3 hops" $ do
- let nodes = [ (testNodePubKey1, emptyHopData)
- , (testNodePubKey2, hopDataWithNextNode)
- , (testNodePubKey3, sampleHopData)
- ]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path ->
- length (bpBlindedHops path) @?= 3
-
- , testCase "all blinded node IDs are 33 bytes" $ do
- let nodes = [ (testNodePubKey1, emptyHopData)
- , (testNodePubKey2, emptyHopData)
- , (testNodePubKey3, emptyHopData)
- ]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> do
- let blindedIds =
- map bhBlindedNodeId (bpBlindedHops path)
- mapM_ (\bid -> BS.length bid @?= 33) blindedIds
-
- , testCase "empty path returns EmptyPath error" $ do
- case createBlindedPath testSeed [] of
- Left EmptyPath -> return ()
- Left err ->
- assertFailure $
- "Expected EmptyPath, got: " ++ show err
- Right _ ->
- assertFailure "Expected error, got success"
-
- , testCase "invalid seed returns InvalidSeed error" $ do
- let invalidSeed = BS.pack [1..16]
- nodes = [(testNodePubKey1, emptyHopData)]
- case createBlindedPath invalidSeed nodes of
- Left InvalidSeed -> return ()
- Left err ->
- assertFailure $
- "Expected InvalidSeed, got: " ++ show err
- Right _ ->
- assertFailure "Expected error, got success"
-
- , testCase "createBlindedPath is deterministic" $ do
- let nodes = [(testNodePubKey1, emptyHopData),
- (testNodePubKey2, sampleHopData)]
- result1 = createBlindedPath testSeed nodes
- result2 = createBlindedPath testSeed nodes
- result1 @?= result2
+ where
+ assertConstructError e r = case r of
+ Left e' -> e' @?= e
+ Right _ -> assertFailure ("expected " ++ show e)
+
+-- process --------------------------------------------------------------------
+
+process_tests :: TestTree
+process_tests = testGroup "process" [
+ testCase "rejects versions other than 0" $ do
+ (node, pkt) <- simple_onion
+ expect_process (InvalidVersion 1)
+ (process node pkt { onion_version = 1 } ad Nothing)
+ , testCase "rejects a public key not on the curve" $ do
+ (node, pkt) <- simple_onion
+ bad <- off_curve
+ expect_process InvalidPublicKey
+ (process node pkt { onion_public_key = bad } ad Nothing)
+ , testCase "rejects a wrong HMAC" $ do
+ (node, pkt) <- simple_onion
+ flipped <- demand "flip" $
+ case BS.uncons (un_hop_payloads (onion_hop_payloads pkt)) of
+ Just (h, t) -> hop_payloads (BS.cons (h `xor` 1) t)
+ Nothing -> Nothing
+ expect_process HmacMismatch
+ (process node pkt { onion_hop_payloads = flipped } ad Nothing)
+ mac <- demand "mac" (hmac32 (BS.replicate 32 0))
+ expect_process HmacMismatch
+ (process node pkt { onion_hmac = mac } ad Nothing)
+ , testCase "rejects other associated data" $ do
+ (node, pkt) <- simple_onion
+ expect_process HmacMismatch
+ (process node pkt (BS.replicate 32 0) Nothing)
+ , testCase "rejects other node keys" $ do
+ (_, pkt) <- simple_onion
+ other <- key 0x44
+ expect_process HmacMismatch (process other pkt ad Nothing)
+ , testCase "rejects payload lengths 0 and 1" $ do
+ node <- key 0x42
+ p0 <- raw_onion node (pad1300 "\NUL")
+ expect_process InvalidPayloadLength (process node p0 ad Nothing)
+ p1 <- raw_onion node (pad1300 "\SOH\SOH")
+ expect_process InvalidPayloadLength (process node p1 ad Nothing)
+ , testCase "rejects a malformed length prefix" $ do
+ node <- key 0x42
+ p <- raw_onion node (pad1300 "\253\NUL\DLE")
+ expect_process InvalidPayloadLength (process node p ad Nothing)
+ , testCase "rejects payloads past the end of hop_payloads" $ do
+ node <- key 0x42
+ -- 3-byte prefix, 1265-byte payload and 32-byte HMAC fill it
+ let rec n = BS.concat [ "\SOH\253", BOLT1.encode_u16 n
+ , BS.replicate (fromIntegral n) 0 ]
+ ok <- raw_onion node (pad1300 ("\253\EOT\241" <> rec 1261))
+ _ <- receive (process node ok ad Nothing)
+ over <- raw_onion node (pad1300 ("\253\EOT\242" <> rec 1262))
+ expect_process InvalidPayloadLength (process node over ad Nothing)
+ -- a length reaching into the zero-extended stream
+ far <- raw_onion node (pad1300 ("\253\a\208" <> rec 1261))
+ expect_process InvalidPayloadLength (process node far ad Nothing)
+ huge <- raw_onion node
+ (pad1300 ("\255\128\NUL\NUL\NUL\NUL\NUL\NUL\NUL" <> rec 1))
+ expect_process InvalidPayloadLength (process node huge ad Nothing)
+ , testCase "rejects malformed payloads" $ do
+ node <- key 0x42
+ let bad pl = raw_onion node
+ (pad1300 (BOLT1.encode_bigsize (fromIntegral (BS.length pl))
+ <> pl))
+ p0 <- bad "\EOT\SOH\SOH\STX\SOH\SOH"
+ expect_process
+ (InvalidPayload (InvalidTlvStream BOLT1.TlvNotStrictlyIncreasing))
+ (process node p0 ad Nothing)
+ p1 <- bad "\DC4\NUL"
+ expect_process
+ (InvalidPayload (InvalidTlvStream (BOLT1.TlvUnknownEvenType 20)))
+ (process node p1 ad Nothing)
+ p2 <- bad "\STX\STX\NUL\SOH"
+ expect_process (InvalidPayload (InvalidTlvValue 2))
+ (process node p2 ad Nothing)
+ , testCase "the final hop receives" $ do
+ (node, pkt) <- simple_onion
+ info <- receive (process node pkt ad Nothing)
+ rcv_payload info @?= cltv_payload 144
+ rcv_blinded info @?= Nothing
+ , testCase "forwarded packets are well formed" $ do
+ sk <- key 0x41
+ a <- key 0x42
+ b <- key 0x43
+ (pkt, sss) <- right "construct" $ construct sk
+ [Hop (public_key a) (cltv_payload 1), Hop (public_key b)
+ (cltv_payload 2)] ad
+ fwd <- forward (process a pkt ad Nothing)
+ let next = fwd_next_packet fwd
+ onion_version next @?= 0
+ decode_onion_packet (encode_onion_packet next) @?= Right next
+ Just (fwd_shared_secret fwd) @?= safe_head sss
+ rcv <- receive (process b next ad Nothing)
+ rcv_payload rcv @?= cltv_payload 2
+ , testCase "path keys without encrypted_recipient_data" $ do
+ sk <- key 0x41
+ alice <- key 0x42
+ seed <- key 0x01
+ path <- right "path" $
+ create_blinded_path seed [(public_key alice, empty_blinded_hop_data)]
+ bid <- blinded_id path 0
+ let pk = bp_first_path_key path
+ -- a path_key, onion encrypted to the blinded id
+ (p0, _) <- right "construct" $
+ construct sk [Hop bid (cltv_payload 1)] ad
+ expect_process UnexpectedPathKey (process alice p0 ad (Just pk))
+ -- a current_path_key
+ let pl = (cltv_payload 1) { hp_current_path_key = Just pk }
+ (p1, _) <- right "construct" $
+ construct sk [Hop (public_key alice) pl] ad
+ expect_process UnexpectedPathKey (process alice p1 ad Nothing)
+ , testCase "both a path_key and a current_path_key" $ do
+ sk <- key 0x41
+ alice <- key 0x42
+ seed <- key 0x01
+ path <- right "path" $
+ create_blinded_path seed [(public_key alice, empty_blinded_hop_data)]
+ bid <- blinded_id path 0
+ enc <- blinded_data path 0
+ let pk = bp_first_path_key path
+ pl = empty_hop_payload { hp_encrypted_data = Just enc
+ , hp_current_path_key = Just pk }
+ (pkt, _) <- right "construct" $ construct sk [Hop bid pl] ad
+ expect_process UnexpectedPathKey (process alice pkt ad (Just pk))
+ , testCase "encrypted_recipient_data without a path key" $ do
+ sk <- key 0x41
+ alice <- key 0x42
+ let pl = empty_hop_payload { hp_encrypted_data = Just "data" }
+ (pkt, _) <- right "construct" $
+ construct sk [Hop (public_key alice) pl] ad
+ expect_process MissingPathKey (process alice pkt ad Nothing)
+ , testCase "undecryptable encrypted_recipient_data" $ do
+ sk <- key 0x41
+ alice <- key 0x42
+ pk <- hex_point V.rbBobPathKey
+ let pl = empty_hop_payload { hp_encrypted_data = Just
+ (BS.replicate 40 0)
+ , hp_current_path_key = Just pk }
+ (pkt, _) <- right "construct" $
+ construct sk [Hop (public_key alice) pl] ad
+ expect_process InvalidRecipientData (process alice pkt ad Nothing)
+ , testCase "invalid path keys" $ do
+ sk <- key 0x41
+ alice <- key 0x42
+ bad <- off_curve
+ let pl = empty_hop_payload { hp_encrypted_data = Just "data"
+ , hp_current_path_key = Just bad }
+ (pkt, _) <- right "construct" $
+ construct sk [Hop (public_key alice) pl] ad
+ expect_process InvalidPathKey (process alice pkt ad Nothing)
+ (pkt', _) <- right "construct" $
+ construct sk [Hop (public_key alice) (cltv_payload 1)] ad
+ expect_process InvalidPathKey (process alice pkt' ad (Just bad))
]
-
--- 6. processBlindedHop Tests ----------------------------------------------
-
-blindingProcessHopTests :: TestTree
-blindingProcessHopTests =
- testGroup "processBlindedHop" [
- testCase "process first hop decrypts correctly" $ do
- let nodes = [(testNodePubKey1, sampleHopData),
- (testNodePubKey2, emptyHopData)]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> case bpBlindedHops path of
- firstHop : _ -> do
- let pathKey = bpBlindingKey path
- case processBlindedHop testNodeSecKey1
- pathKey (bhEncryptedData firstHop) of
- Left err -> assertFailure $
- "processBlindedHop failed: " ++ show err
- Right (decryptedData, _) ->
- decryptedData @?= sampleHopData
- [] -> assertFailure "expected non-empty hops"
-
- , testCase "process hop chain correctly" $ do
- let nodes =
- [ (testNodePubKey1, emptyHopData)
- , (testNodePubKey2, sampleHopData)
- , (testNodePubKey3, hopDataWithNextNode)
- ]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> case bpBlindedHops path of
- [hop1, hop2, hop3] -> do
- let pathKey1 = bpBlindingKey path
- case processBlindedHop testNodeSecKey1
- pathKey1 (bhEncryptedData hop1) of
- Left err -> assertFailure $
- "processBlindedHop hop1 failed: "
- ++ show err
- Right (data1, pathKey2) -> do
- data1 @?= emptyHopData
- case processBlindedHop testNodeSecKey2
- pathKey2
- (bhEncryptedData hop2) of
- Left err -> assertFailure $
- "processBlindedHop hop2 failed: "
- ++ show err
- Right (data2, pathKey3) -> do
- data2 @?= sampleHopData
- case processBlindedHop
- testNodeSecKey3 pathKey3
- (bhEncryptedData hop3) of
- Left err -> assertFailure $
- "processBlindedHop hop3: "
- ++ show err
- Right (data3, _) ->
- data3 @?= hopDataWithNextNode
- _ -> assertFailure "expected 3 blinded hops"
-
- , testCase "process hop with wrong node key fails" $ do
- let nodes = [(testNodePubKey1, sampleHopData)]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> case bpBlindedHops path of
- firstHop : _ -> do
- let pathKey = bpBlindingKey path
- case processBlindedHop testNodeSecKey2
- pathKey (bhEncryptedData firstHop) of
- Left _ -> return ()
- Right (decryptedData, _) ->
- assertBool "should not decrypt correctly"
- (decryptedData /= sampleHopData)
- [] -> assertFailure "expected non-empty hops"
-
- , testCase "next path key is valid point" $ do
- let nodes = [(testNodePubKey1, emptyHopData),
- (testNodePubKey2, emptyHopData)]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> case bpBlindedHops path of
- firstHop : _ -> do
- let pathKey = bpBlindingKey path
- case processBlindedHop testNodeSecKey1
- pathKey (bhEncryptedData firstHop) of
- Left err -> assertFailure $
- "processBlindedHop failed: " ++ show err
- Right (_, nextPK) -> do
- let serialized =
- Secp256k1.serialize_point nextPK
- BS.length serialized @?= 33
- [] -> assertFailure "expected non-empty hops"
-
- , testCase "next_path_key_override is used" $ do
- let overrideKey =
- Secp256k1.serialize_point testNodePubKey3
- hopDataWithOverride' = emptyHopData
- { bhdNextPathKeyOverride = Just overrideKey }
- nodes = [(testNodePubKey1, hopDataWithOverride'),
- (testNodePubKey2, emptyHopData)]
- case createBlindedPath testSeed nodes of
- Left err ->
- assertFailure $
- "createBlindedPath failed: " ++ show err
- Right path -> case bpBlindedHops path of
- firstHop : _ -> do
- let pathKey = bpBlindingKey path
- case processBlindedHop testNodeSecKey1
- pathKey (bhEncryptedData firstHop) of
- Left err -> assertFailure $
- "processBlindedHop failed: " ++ show err
- Right (decryptedData, nextPK) -> do
- bhdNextPathKeyOverride decryptedData
- @?= Just overrideKey
- nextPK @?= testNodePubKey3
- [] -> assertFailure "expected non-empty hops"
+ where
+ simple_onion = do
+ sk <- key 0x41
+ node <- key 0x42
+ (pkt, _) <- right "construct" $
+ construct sk [Hop (public_key node) (cltv_payload 144)] ad
+ pure (node, pkt)
+
+three :: [a] -> IO (a, a, a)
+three xs = case xs of
+ [a, b, c] -> pure (a, b, c)
+ _ -> assertFailure "expected three elements"
+
+safe_head :: [a] -> Maybe a
+safe_head xs = case xs of
+ x : _ -> Just x
+ [] -> Nothing
+
+safe_index :: [a] -> Int -> Maybe a
+safe_index xs i = safe_head (drop i xs)
+
+blinded_id :: BlindedPath -> Int -> IO BOLT1.Point
+blinded_id p i =
+ demand "blinded hop" (bh_blinded_node_id <$> safe_index (bp_hops p) i)
+
+blinded_data :: BlindedPath -> Int -> IO BS.ByteString
+blinded_data p i =
+ demand "blinded hop" (bh_encrypted_data <$> safe_index (bp_hops p) i)
+
+-- route blinding -------------------------------------------------------------
+
+blinding_tests :: TestTree
+blinding_tests = testGroup "route blinding" [
+ testCase "empty path" $ do
+ seed <- key 0x01
+ create_blinded_path seed [] @?= Left EmptyPath
+ , testCase "invalid node id" $ do
+ seed <- key 0x01
+ alice <- key 0x42
+ bad <- off_curve
+ create_blinded_path seed
+ [ (public_key alice, empty_blinded_hop_data)
+ , (bad, empty_blinded_hop_data) ]
+ @?= Left (InvalidNodeId 1)
+ , testCase "unencodable hop data" $ do
+ seed <- key 0x01
+ alice <- key 0x42
+ ex <- tlvs [BOLT1.TlvRecord 6 "id"]
+ create_blinded_path seed
+ [(public_key alice, empty_blinded_hop_data { bhd_extra = ex })]
+ @?= Left (InvalidHopData 0 ConflictingTlv)
+ , testCase "decrypt_recipient_data rejects bad input" $ do
+ seed <- key 0x01
+ alice <- key 0x42
+ bob <- key 0x43
+ path <- right "path" $
+ create_blinded_path seed [(public_key alice, empty_blinded_hop_data)]
+ enc <- blinded_data path 0
+ let pk = bp_first_path_key path
+ _ <- right "decrypt" (decrypt_recipient_data alice pk enc)
+ decrypt_recipient_data bob pk enc @?= Left InvalidRecipientData
+ decrypt_recipient_data alice pk (BS.take 15 enc) @?=
+ Left InvalidRecipientData
+ decrypt_recipient_data alice pk (BS.drop 1 enc) @?=
+ Left InvalidRecipientData
+ bad <- off_curve
+ decrypt_recipient_data alice bad enc @?= Left InvalidPathKey
+ , testCase "unknown even types in encrypted data are rejected" $ do
+ seed <- key 0x01
+ alice <- key 0x42
+ ex <- tlvs [BOLT1.TlvRecord 20 ""]
+ path <- right "path" $ create_blinded_path seed
+ [(public_key alice, empty_blinded_hop_data { bhd_extra = ex })]
+ enc <- blinded_data path 0
+ decrypt_recipient_data alice (bp_first_path_key path) enc @?=
+ Left InvalidRecipientData
+ , testCase "a payment through a blinded route" $ do
+ -- Carol creates a route Alice -> Bob -> Carol; Dave pays through it,
+ -- reaching Alice unblinded with current_path_key.
+ seed <- key 0x01
+ session <- key 0x02
+ alice <- key 0x42
+ bob <- key 0x43
+ carol <- key 0x44
+ s1 <- scid 1 1 1
+ s2 <- scid 2 2 2
+ m <- msat 1
+ let relay = Just (PaymentRelay 40 100 1000)
+ cons = Just (PaymentConstraints 900000 m)
+ alice_data = empty_blinded_hop_data
+ { bhd_short_channel_id = Just s1, bhd_payment_relay = relay
+ , bhd_payment_constraints = cons }
+ bob_data = empty_blinded_hop_data
+ { bhd_short_channel_id = Just s2, bhd_payment_relay = relay
+ , bhd_payment_constraints = cons }
+ carol_data = empty_blinded_hop_data
+ { bhd_path_id = Just "invoice" }
+ path <- right "path" $ create_blinded_path seed
+ [ (public_key alice, alice_data), (public_key bob, bob_data)
+ , (public_key carol, carol_data) ]
+ bp_first_node_id path @?= public_key alice
+ bp_first_path_key path @?= public_key seed
+ (e0, e1, e2) <- three =<< mapM (blinded_data path) [0, 1, 2]
+ (_, b1, b2) <- three =<< mapM (blinded_id path) [0, 1, 2]
+ amt <- msat 5000
+ let intro = empty_hop_payload
+ { hp_encrypted_data = Just e0
+ , hp_current_path_key = Just (bp_first_path_key path) }
+ final = empty_hop_payload
+ { hp_encrypted_data = Just e2, hp_amt_to_forward = Just amt
+ , hp_outgoing_cltv_value = Just 800000
+ , hp_total_amount_msat = Just amt }
+ route = [ Hop (public_key alice) intro
+ , Hop b1 empty_hop_payload { hp_encrypted_data = Just e1 }
+ , Hop b2 final ]
+ (pkt, sss) <- right "construct" (construct session route ad)
+ fa <- forward (process alice pkt ad Nothing)
+ ia <- demand "alice blinded" (fwd_blinded fa)
+ bi_data ia @?= alice_data
+ fb <- forward (process bob (fwd_next_packet fa) ad
+ (Just (bi_next_path_key ia)))
+ ib <- demand "bob blinded" (fwd_blinded fb)
+ bi_data ib @?= bob_data
+ rc <- receive (process carol (fwd_next_packet fb) ad
+ (Just (bi_next_path_key ib)))
+ ic <- demand "carol blinded" (rcv_blinded rc)
+ bi_data ic @?= carol_data
+ rcv_payload rc @?= final
+ [fwd_shared_secret fa, fwd_shared_secret fb, rcv_shared_secret rc]
+ @?= sss
+ -- the wrong path key yields the wrong blinded key
+ expect_process HmacMismatch
+ (process bob (fwd_next_packet fa) ad
+ (Just (bp_first_path_key path)))
]
--- Property tests ------------------------------------------------------------
-
-propertyTests :: TestTree
-propertyTests = testGroup "invariants" [
- testProperty "ShortChannelId encode/decode roundtrip"
- propScidRoundtrip
- , testProperty "HopPayload encode/decode roundtrip"
- propHopPayloadRoundtrip
- , testProperty "fixed-size newtypes validate length"
- propNewtypeValidation
- , testProperty "FailureMessage encode/decode roundtrip"
- propFailureMessageRoundtrip
+-- returning errors -----------------------------------------------------------
+
+error_tests :: TestTree
+error_tests = testGroup "returning errors" [
+ testCase "padding brings failure_len + pad_len to 256" $ do
+ ss <- demand "ss" (shared_secret (BS.replicate 32 1))
+ let fm = FailureMessage TemporaryNodeFailure no_tlvs
+ pkt <- right "construct_error" (construct_error ss fm)
+ let ErrorPacket raw = wrap_error ss pkt
+ BS.length raw @?= 32 + 2 + 2 + 2 + 254
+ BS.take 4 (BS.drop 32 raw) @?= "\NUL\STX\x20\x02"
+ BS.take 2 (BS.drop 36 raw) @?= "\NUL\254"
+ , testCase "long failure messages are not padded" $ do
+ ss <- demand "ss" (shared_secret (BS.replicate 32 1))
+ ex <- tlvs [BOLT1.TlvRecord 1 (BS.replicate 300 0)]
+ let fm = FailureMessage TemporaryNodeFailure ex
+ ErrorPacket raw <- wrap_error ss <$>
+ right "construct_error" (construct_error ss fm)
+ BS.length raw @?= 32 + 2 + 306 + 2
+ BS.drop (32 + 2 + 306) raw @?= "\NUL\NUL"
+ , testCase "failure messages over 65535 bytes" $ do
+ ss <- demand "ss" (shared_secret (BS.replicate 32 1))
+ ex <- tlvs [BOLT1.TlvRecord 1 (BS.replicate 65535 0)]
+ construct_error ss (FailureMessage TemporaryNodeFailure ex)
+ @?= Left FieldTooLong
+ , testCase "attributes the failing hop" $ do
+ sss <- mapM (\b -> demand "ss" (shared_secret (BS.replicate 32 b)))
+ [1 .. 5]
+ let fm = FailureMessage PermanentChannelFailure no_tlvs
+ forM_ (zip [0 ..] sss) $ \(i, ss) -> do
+ pkt <- right "construct_error" (construct_error ss fm)
+ let back = foldr wrap_error pkt (take i sss)
+ unwrap_error sss back @?= Attributed i fm
+ , testCase "reports the hop of a malformed failure" $ do
+ sss <- mapM (\b -> demand "ss" (shared_secret (BS.replicate 32 b)))
+ [1 .. 3]
+ ss2 <- demand "ss2" (safe_index sss 2)
+ -- incorrect_cltv_expiry with no data
+ let fm = FailureMessage (UnknownFailure 0x100d "") no_tlvs
+ pkt <- right "construct_error" (construct_error ss2 fm)
+ let back = foldr wrap_error pkt (take 2 sss)
+ unwrap_error sss back @?= MalformedFailure 2
+ , testCase "reports a malformed return packet" $ do
+ ss <- demand "ss" (shared_secret (BS.replicate 32 1))
+ -- a valid HMAC over a body whose failure_len overruns it
+ let body = "\SOH\NUL\x20\x02"
+ SHA256.MAC um = SHA256.hmac "um" (un_shared_secret ss)
+ SHA256.MAC mac = SHA256.hmac um body
+ pkt = wrap_error ss (ErrorPacket (mac <> body))
+ unwrap_error [ss] pkt @?= MalformedFailure 0
+ , testCase "unknown origin" $ do
+ sss <- mapM (\b -> demand "ss" (shared_secret (BS.replicate 32 b)))
+ [1 .. 3]
+ other <- demand "ss" (shared_secret (BS.replicate 32 9))
+ let fm = FailureMessage TemporaryNodeFailure no_tlvs
+ pkt <- right "construct_error" (construct_error other fm)
+ unwrap_error sss pkt @?= UnknownOrigin
+ unwrap_error [] pkt @?= UnknownOrigin
+ unwrap_error sss (ErrorPacket "short") @?= UnknownOrigin
]
-propScidRoundtrip :: Property
-propScidRoundtrip =
- forAll (choose (0, 0xFFFFFF)) $ \bh ->
- forAll (choose (0, 0xFFFFFF)) $ \ti ->
- forAll arbitrary $ \oi ->
- case shortChannelId bh ti oi of
- Nothing -> False
- Just scid ->
- let encoded = encodeShortChannelId scid
- in decodeShortChannelId encoded == Just scid
-
-propHopPayloadRoundtrip :: Property
-propHopPayloadRoundtrip =
- forAll genHopPayload $ \hp ->
- let encoded = encodeHopPayload hp
- in decodeHopPayload encoded == Just hp
-
-genHopPayload :: Gen HopPayload
-genHopPayload = do
- amt <- oneof [pure Nothing, Just <$> arbitrary]
- cltv <- oneof [pure Nothing, Just <$> arbitrary]
- sci <- oneof [pure Nothing, genScid]
- pure HopPayload
- { hpAmtToForward = amt
- , hpOutgoingCltv = cltv
- , hpShortChannelId = sci
- , hpPaymentData = Nothing
- , hpEncryptedData = Nothing
- , hpCurrentPathKey = Nothing
- , hpUnknownTlvs = []
- }
- where
- genScid :: Gen (Maybe ShortChannelId)
- genScid = do
- bh <- choose (0, 0xFFFFFF)
- ti <- choose (0, 0xFFFFFF)
- oi <- arbitrary
- pure (shortChannelId bh ti oi)
-
-propNewtypeValidation :: NonNegative Int -> Property
-propNewtypeValidation (NonNegative n) = property $
- let len = n `mod` 2000
- bs = BS.replicate len 0x00
- h32 = hmac32 bs
- hp = hopPayloads bs
- ps = paymentSecret bs
- in (case h32 of
- Just _ -> len == 32
- Nothing -> len /= 32)
- &&
- (case hp of
- Just _ -> len == hopPayloadsSize
- Nothing -> len /= hopPayloadsSize)
- &&
- (case ps of
- Just _ -> len == 32
- Nothing -> len /= 32)
-
-propFailureMessageRoundtrip :: Property
-propFailureMessageRoundtrip =
- forAll genFailureMessage $ \fm ->
- let encoded = encodeFailureMessage fm
- in decodeFailureMessage encoded == Just fm
-
-genFailureMessage :: Gen FailureMessage
-genFailureMessage = do
- code <- elements
- [ InvalidRealm
- , TemporaryNodeFailure
- , PermanentNodeFailure
- , RequiredNodeFeatureMissing
- , InvalidOnionVersion
- , InvalidOnionHmac
- , InvalidOnionKey
- , TemporaryChannelFailure
- , PermanentChannelFailure
- , AmountBelowMinimum
- , FeeInsufficient
- , IncorrectCltvExpiry
- , ExpiryTooSoon
- , IncorrectOrUnknownPaymentDetails
- , FinalIncorrectCltvExpiry
- , FinalIncorrectHtlcAmount
- , ChannelDisabled
- , ExpiryTooFar
- , InvalidOnionPayload
- , MppTimeout
- ]
- dat <- genFailureData code
- ntlv <- choose (0, 3 :: Int)
- types <- (nub . sort) <$> vectorOf ntlv arbitrary
- tlvs <- mapM (\t -> do
- len <- choose (0, 20 :: Int)
- TlvRecord t . BS.pack <$> vectorOf len arbitrary) types
- pure (FailureMessage code dat tlvs)
-
--- | Random well-formed data for a failure code.
-genFailureData :: FailureCode -> Gen BS.ByteString
-genFailureData c
- | c `elem` [InvalidOnionVersion, InvalidOnionHmac, InvalidOnionKey] =
- bytes 32
- | c `elem` [TemporaryChannelFailure, ExpiryTooSoon] = update 0
- | c `elem` [AmountBelowMinimum, FeeInsufficient] = update 8
- | c == IncorrectCltvExpiry = update 4
- | c == ChannelDisabled = update 2
- | c == IncorrectOrUnknownPaymentDetails = bytes 12
- | c == FinalIncorrectCltvExpiry = bytes 4
- | c == FinalIncorrectHtlcAmount = bytes 8
- | c == InvalidOnionPayload = do
- typ <- arbitrary
- off <- bytes 2
- pure (encodeBigSize typ <> off)
- | otherwise = pure BS.empty
- where
- bytes n = BS.pack <$> vectorOf n arbitrary
- update n = do
- fields <- bytes n
- len <- choose (0, 300 :: Int)
- cu <- bytes len
- pure $ fields
- <> BS.pack [fromIntegral (len `div` 256), fromIntegral len]
- <> cu
-
--- Spec vectors: onion-test.json --------------------------------------------
-
--- | Shared secrets for the onion-test.json route (identical to those
--- listed in onion-error-test.json).
-onionSharedSecrets :: IO [BS.ByteString]
-onionSharedSecrets = mapM unhex
- [ V.errSharedSecret0, V.errSharedSecret1, V.errSharedSecret2
- , V.errSharedSecret3, V.errSharedSecret4 ]
+-- spec vectors: onion-test.json ----------------------------------------------
--- | (node key, node pubkey, payload without length prefix) per hop.
-onionVectorHops
- :: IO [(BS.ByteString, Secp256k1.Projective, BS.ByteString)]
-onionVectorHops = mapM hop
+onion_hops :: IO [(SecretKey, BOLT1.Point, BS.ByteString)]
+onion_hops = mapM hop
[ (V.onionNodeKey0, V.onionPubKey0, V.onionPayload0)
, (V.onionNodeKey1, V.onionPubKey1, V.onionPayload1)
, (V.onionNodeKey2, V.onionPubKey2, V.onionPayload2)
@@ -1709,585 +870,596 @@ onionVectorHops = mapM hop
, (V.onionNodeKey4, V.onionPubKey4, V.onionPayload4)
]
where
- hop (k, p, pl) = do
- key <- unhex k
- pub <- unhexPoint p
- body <- unhex pl >>= unprefix
- pure (key, pub, body)
-
--- | Next ephemeral key per spec: SHA256(E || ss) * E.
-nextEphemeralKey
- :: Secp256k1.Projective -> BS.ByteString -> Maybe Secp256k1.Projective
-nextEphemeralKey e ss = do
- tweak <- Secp256k1.roll32
- (SHA256.hash (Secp256k1.serialize_point e <> ss))
- Secp256k1.mul e tweak
-
--- | Process an onion hop by hop, checking each hop's payload and
--- shared secret, and that each forwarded packet is a well-formed
--- 1366-byte packet carrying the expected ephemeral key.
-peelOnion
- :: BS.ByteString
- -> [(BS.ByteString, BS.ByteString, BS.ByteString)]
- -- ^ (node key, expected payload, expected shared secret)
- -> Secp256k1.Projective
- -- ^ expected ephemeral key of the packet
- -> OnionPacket
- -> Assertion
-peelOnion _ [] _ _ = assertFailure "no hops"
-peelOnion ad ((key, body, ss) : rest) eph pkt = do
- opVersion pkt @?= versionByte
- opEphemeralKey pkt @?= Secp256k1.serialize_point eph
- case (process key pkt ad, rest) of
- (Left e, _) -> assertFailure ("process failed: " ++ show e)
- (Right (Receive ri), []) -> do
- encodeHopPayload (riPayload ri) @?= body
- riSharedSecret ri @?= SharedSecret ss
- (Right (Forward fi), _ : _) -> do
- encodeHopPayload (fiPayload fi) @?= body
- fiSharedSecret fi @?= SharedSecret ss
- let next = fiNextPacket fi
- bytes = encodeOnionPacket next
- BS.length bytes @?= onionPacketSize
- decodeOnionPacket bytes @?= Just next
- eph' <- demand "next ephemeral key" (nextEphemeralKey eph ss)
- peelOnion ad rest eph' next
- (Right (Receive _), _ : _) ->
- assertFailure "expected Forward, got Receive"
- (Right (Forward _), []) ->
- assertFailure "expected Receive, got Forward"
-
-onionVectorTests :: TestTree
-onionVectorTests = testGroup "onion-test.json" [
- testCase "construct reproduces the spec onion" $ do
- hops <- onionVectorHops
- sk <- unhex V.onionSessionKey
- ad <- unhex V.onionAssocData
+ hop (k, p, pl) = (,,) <$> hex_key k <*> hex_point p
+ <*> (unhex pl >>= unprefix)
+
+-- the shared secrets of the onion-test.json route, as listed in
+-- onion-error-test.json
+route_secrets :: IO [SharedSecret]
+route_secrets = mapM hex_secret
+ [ V.errSharedSecret0, V.errSharedSecret1, V.errSharedSecret2
+ , V.errSharedSecret3, V.errSharedSecret4 ]
+
+onion_vectors :: TestTree
+onion_vectors = testGroup "onion-test.json" [
+ testCase "construct reproduces the onion" $ do
+ hops <- onion_hops
+ sk <- hex_key V.onionSessionKey
+ assoc <- unhex V.onionAssocData
expected <- unhex V.onionPacket
- sss <- onionSharedSecrets
+ sss <- route_secrets
route <- mapM (\(_, pub, body) -> do
- hp <- demand "decodeHopPayload" (decodeHopPayload body)
- encodeHopPayload hp @?= body
+ hp <- right "decode_hop_payload" (decode_hop_payload body)
+ encode_hop_payload hp @?= Right body
pure (Hop pub hp)) hops
- case construct sk route ad of
- Left e -> assertFailure ("construct failed: " ++ show e)
- Right (pkt, secrets) -> do
- encodeOnionPacket pkt @?= expected
- secrets @?= map SharedSecret sss
-
- , testCase "process peels the spec onion at every hop" $ do
- hops <- onionVectorHops
- sk <- unhex V.onionSessionKey
- ad <- unhex V.onionAssocData
- sss <- onionSharedSecrets
- pkt <- unhex V.onionPacket >>=
- demand "decodeOnionPacket" . decodeOnionPacket
- e0 <- demand "session pubkey"
- (Secp256k1.roll32 sk >>= Secp256k1.derive_pub)
- let expect = zipWith (\(k, _, body) ss -> (k, body, ss)) hops sss
- peelOnion ad expect e0 pkt
+ (pkt, secrets) <- right "construct" (construct sk route assoc)
+ encode_onion_packet pkt @?= expected
+ secrets @?= sss
+ , testCase "process peels the onion at every hop" $ do
+ hops <- onion_hops
+ assoc <- unhex V.onionAssocData
+ sss <- route_secrets
+ pkt <- unhex V.onionPacket >>= right "decode" . decode_onion_packet
+ let go [] _ = assertFailure "no hops"
+ go [((k, _, body), ss)] p = do
+ r <- receive (process k p assoc Nothing)
+ encode_hop_payload (rcv_payload r) @?= Right body
+ rcv_shared_secret r @?= ss
+ go (((k, _, body), ss) : rest) p = do
+ f <- forward (process k p assoc Nothing)
+ encode_hop_payload (fwd_payload f) @?= Right body
+ fwd_shared_secret f @?= ss
+ fwd_blinded f @?= Nothing
+ go rest (fwd_next_packet f)
+ go (zip hops sss) pkt
]
--- Multi-hop construct/process ----------------------------------------------
-
--- | Construct an onion over a route of (node key, payload) pairs, then
--- process it at every hop, checking payloads and shared secrets.
-roundtripRoute :: BS.ByteString -> [(BS.ByteString, HopPayload)] -> Bool
-roundtripRoute sk route = case mapM mkHop route of
- Nothing -> False
- Just hops -> case construct sk hops ad of
- Left _ -> False
- Right (pkt, secrets) ->
- length secrets == length route && go (zip route secrets) pkt
- where
- ad = BS.replicate 32 0x42
- mkHop (k, hp) = do
- pub <- Secp256k1.roll32 k >>= Secp256k1.derive_pub
- pure (Hop pub hp)
- go [] _ = False
- go [((k, hp), ss)] pkt = case process k pkt ad of
- Right (Receive ri) -> riPayload ri == hp && riSharedSecret ri == ss
- _ -> False
- go (((k, hp), ss) : rest) pkt = case process k pkt ad of
- Right (Forward fi) ->
- fiPayload fi == hp
- && fiSharedSecret fi == ss
- && BS.length (encodeOnionPacket (fiNextPacket fi))
- == onionPacketSize
- && go rest (fiNextPacket fi)
- _ -> False
-
-multiHopTests :: TestTree
-multiHopTests = testGroup "construct then process" [
- testCase "routes of 1 to 20 hops" $
- mapM_ (\n -> do
- route <- mapM (\i -> do
- scid <- assertScid (700000 + i) i 1
- let hp = HopPayload
- { hpAmtToForward = Just (1000 * fromIntegral i)
- , hpOutgoingCltv = Just (500000 + i)
- , hpShortChannelId = Just scid
- , hpPaymentData = Nothing
- , hpEncryptedData = Nothing
- , hpCurrentPathKey = Nothing
- , hpUnknownTlvs = []
- }
- pure (BS.replicate 32 (fromIntegral i), hp)) [1 .. n]
- assertBool ("route of " ++ show n ++ " hops")
- (roundtripRoute sessionKey route)) [1 .. 20 :: Word32]
-
- , testProperty "random routes of 1 to 20 hops" $
- forAll (choose (1, 20)) $ \n ->
- forAll genSecKey $ \sk ->
- forAll (vectorOf n genSecKey) $ \keys ->
- forAll (vectorOf n (genRoutePayload n)) $ \pls ->
- roundtripRoute sk (zip keys pls)
- ]
-
--- | A valid 32-byte secret key.
-genSecKey :: Gen BS.ByteString
-genSecKey = (BS.pack <$> vectorOf 32 arbitrary) `suchThat` valid
- where
- valid k = isJust (Secp256k1.roll32 k >>= Secp256k1.derive_pub)
+-- spec vectors: onion-error-test.json ----------------------------------------
--- | A hop payload whose shift size fits n hops in 1300 bytes.
---
--- The fixed fields take at most 26 bytes; an odd TLV of type 65537
--- adds at most 8 bytes of framing plus its value. With a 3-byte
--- length prefix and 32-byte HMAC a hop then shifts at most 69 bytes
--- plus the value length.
-genRoutePayload :: Int -> Gen HopPayload
-genRoutePayload n = do
- amt <- arbitrary
- cltv <- arbitrary
- scid <- oneof [pure Nothing, genScid]
- let budget = hopPayloadsSize `div` n - 69
- extra <- if budget < 0
- then pure []
- else do
- len <- choose (0, budget)
- blob <- BS.pack <$> vectorOf len arbitrary
- pure [TlvRecord 65537 blob]
- pure HopPayload
- { hpAmtToForward = Just amt
- , hpOutgoingCltv = Just cltv
- , hpShortChannelId = scid
- , hpPaymentData = Nothing
- , hpEncryptedData = Nothing
- , hpCurrentPathKey = Nothing
- , hpUnknownTlvs = extra
- }
- where
- genScid :: Gen (Maybe ShortChannelId)
- genScid = do
- bh <- choose (0, 0xFFFFFF)
- ti <- choose (0, 0xFFFFFF)
- oi <- arbitrary
- pure (shortChannelId bh ti oi)
-
--- Spec vectors: onion-error-test.json --------------------------------------
-
--- | Shared secrets for hops 0 to 4, in route order.
-errorSharedSecrets :: IO [SharedSecret]
-errorSharedSecrets = map SharedSecret <$> onionSharedSecrets
-
--- | The failing node's (hop 4) shared secret, and those of the hops
--- that wrap its return packet, from hop 3 back to hop 0.
-errorWrappers :: IO (SharedSecret, [SharedSecret])
-errorWrappers = do
- sss <- errorSharedSecrets
+-- the failing node's shared secret, and those of the hops back to the
+-- origin, from hop 3 to hop 0
+error_wrappers :: IO (SharedSecret, [SharedSecret])
+error_wrappers = do
+ sss <- route_secrets
case reverse sss of
ss4 : rest -> pure (ss4, rest)
- [] -> assertFailure "no shared secrets"
-
-errorVectorTests :: TestTree
-errorVectorTests = testGroup "onion-error-test.json" [
- testCase "ammag and um keys" $ do
- sss <- errorSharedSecrets
- ammags <- mapM unhex
- [ V.errAmmagKey0, V.errAmmagKey1, V.errAmmagKey2
- , V.errAmmagKey3, V.errAmmagKey4 ]
- map deriveAmmag sss @?= map DerivedKey ammags
- (ss4, _) <- errorWrappers
- um4 <- unhex V.errUmKey4
- deriveUm ss4 @?= DerivedKey um4
-
- , testCase "failing node builds the spec return packet" $ do
- (ss4, _) <- errorWrappers
+ [] -> assertFailure "no shared secrets"
+
+error_vectors :: TestTree
+error_vectors = testGroup "onion-error-test.json" [
+ testCase "failure message" $ do
msg <- unhex V.errFailureMessage
+ let fm = FailureMessage TemporaryNodeFailure no_tlvs
+ encode_failure_message fm @?= Right msg
+ decode_failure_message msg @?= Right fm
+ , testCase "the failing node's return packet" $ do
+ (ss4, _) <- error_wrappers
payload <- unhex V.errPayload4
- fm <- demand "decodeFailureMessage" (decodeFailureMessage msg)
- fm @?= FailureMessage TemporaryNodeFailure BS.empty []
- encodeFailureMessage fm @?= msg
- -- removing hop 4's own obfuscation leaves hmac || payload
- ErrorPacket raw <- mkError ss4 fm >>= wrapErr ss4
+ let fm = FailureMessage TemporaryNodeFailure no_tlvs
+ pkt <- right "construct_error" (construct_error ss4 fm)
+ -- removing hop 4's obfuscation leaves the HMAC and payload
+ let ErrorPacket raw = wrap_error ss4 pkt
BS.drop 32 raw @?= payload
-
- , testCase "wrapping at each hop yields the spec packet" $ do
- (ss4, wrappers) <- errorWrappers
+ , testCase "wrapping at each hop yields the returned packet" $ do
+ (ss4, wrappers) <- error_wrappers
expected <- unhex V.errPacket
- let fm = FailureMessage TemporaryNodeFailure BS.empty []
- initial <- mkError ss4 fm
- final <- foldM (flip wrapErr) initial wrappers
- final @?= ErrorPacket expected
-
- , testCase "origin attributes the spec packet to hop 4" $ do
- sss <- errorSharedSecrets
+ let fm = FailureMessage TemporaryNodeFailure no_tlvs
+ pkt <- right "construct_error" (construct_error ss4 fm)
+ foldl (flip wrap_error) pkt wrappers @?= ErrorPacket expected
+ , testCase "the origin attributes the packet to hop 4" $ do
+ sss <- route_secrets
pkt <- unhex V.errPacket
- unwrapError sss (ErrorPacket pkt)
- @?= Attributed 4 (FailureMessage TemporaryNodeFailure BS.empty [])
+ unwrap_error sss (ErrorPacket pkt) @?=
+ Attributed 4 (FailureMessage TemporaryNodeFailure no_tlvs)
]
--- Spec vectors: 'Returning Errors' trace -----------------------------------
+-- spec vectors: 'Returning Errors' trace -------------------------------------
--- | incorrect_or_unknown_payment_details with htlc_msat 100, height
--- 800000 and a 300-byte TLV of type 34001.
-traceFailure :: FailureMessage
-traceFailure = FailureMessage IncorrectOrUnknownPaymentDetails
- (BS.pack [0, 0, 0, 0, 0, 0, 0, 100, 0, 0x0c, 0x35, 0x00])
- [TlvRecord 34001 (BS.replicate 300 0x80)]
+trace_failure :: IO FailureMessage
+trace_failure = do
+ m <- msat 100
+ ex <- tlvs [BOLT1.TlvRecord 34001 (BS.replicate 300 0x80)]
+ pure (FailureMessage (IncorrectOrUnknownPaymentDetails m 800000) ex)
-traceVectorTests :: TestTree
-traceVectorTests = testGroup "returning errors trace" [
+trace_vectors :: TestTree
+trace_vectors = testGroup "returning errors trace" [
testCase "failure message encoding" $ do
+ fm <- trace_failure
-- the trace lists failuremsg || pad_len || pad
encoded <- unhex V.traceFailureMessage
- raw <- unhex V.traceRawPacket
- let msg = encodeFailureMessage traceFailure
- BS.take (BS.length msg) encoded @?= msg
- decodeFailureMessage msg @?= Just traceFailure
- failureLen <- u16At 32 raw
- failureLen @?= BS.length msg
- BS.take failureLen (BS.drop 34 raw) @?= msg
-
+ msg <- right "encode" (encode_failure_message fm)
+ BS.length msg @?= 320
+ BS.take 320 encoded @?= msg
+ decode_failure_message msg @?= Right fm
, testCase "each hop's packet matches the trace" $ do
- (ss4, wrappers) <- errorWrappers
+ (ss4, wrappers) <- error_wrappers
raw <- unhex V.traceRawPacket
expected <- mapM unhex
[ V.tracePacket4, V.tracePacket3, V.tracePacket2
, V.tracePacket1, V.tracePacket0 ]
- packets <- demand "wrapError" $ sequence $ drop 1 $
- scanl (\p ss -> p >>= wrapError ss) (Just (ErrorPacket raw))
- (ss4 : wrappers)
+ let packets = drop 1 $
+ scanl (flip wrap_error) (ErrorPacket raw) (ss4 : wrappers)
packets @?= map ErrorPacket expected
-
- , testCase "origin attributes the trace packet to hop 4" $ do
- sss <- errorSharedSecrets
+ , testCase "the origin attributes the packet to hop 4" $ do
+ fm <- trace_failure
+ sss <- route_secrets
pkt <- unhex V.tracePacket0
- unwrapError sss (ErrorPacket pkt) @?= Attributed 4 traceFailure
+ unwrap_error sss (ErrorPacket pkt) @?= Attributed 4 fm
]
--- Spec vectors: route-blinding-test.json -----------------------------------
-
--- | Blinded private key HMAC256("blinded_node_id", ss) * k, where
--- ss = ECDH(k, path_key). The library has no function for this, so
--- it is assembled from exported primitives.
-blindedPrivKey
- :: BS.ByteString -> Secp256k1.Projective -> Maybe BS.ByteString
-blindedPrivKey k pathKey = do
- SharedSecret ss <- computeSharedSecret k pathKey
- let SHA256.MAC tweak = SHA256.hmac "blinded_node_id" ss
- blindSecKey k (BlindingFactor tweak)
-
-rbBobData :: IO BlindedHopData
-rbBobData = do
- scid <- assertScid 0 0 1729
- pure emptyHopData
- { bhdPadding = Just (BS.replicate 26 0)
- , bhdShortChannelId = Just scid
- , bhdPaymentRelay = Just (PaymentRelay 36 150 10000)
- , bhdPaymentConstraints = Just (PaymentConstraints 748005 1500)
- , bhdAllowedFeatures = Just BS.empty
+-- spec vectors: route-blinding-test.json -------------------------------------
+
+empty_features :: Maybe BOLT9.FeatureVector
+empty_features = Just (BOLT9.parse BS.empty)
+
+rb_bob_data :: IO BlindedHopData
+rb_bob_data = do
+ s <- scid 0 0 1729
+ m <- msat 1500
+ ex <- tlvs [BOLT1.TlvRecord 561 "\DC24V"]
+ pure empty_blinded_hop_data
+ { bhd_padding = Just (BS.replicate 26 0)
+ , bhd_short_channel_id = Just s
+ , bhd_payment_relay = Just (PaymentRelay 36 150 10000)
+ , bhd_payment_constraints = Just (PaymentConstraints 748005 m)
+ , bhd_allowed_features = empty_features
+ , bhd_extra = ex
}
-rbCarolData :: IO BlindedHopData
-rbCarolData = do
- scid <- assertScid 0 0 1105
- override <- unhex V.rbDavePathKey
- pure emptyHopData
- { bhdShortChannelId = Just scid
- , bhdNextPathKeyOverride = Just override
- , bhdPaymentRelay = Just (PaymentRelay 48 100 500)
- , bhdPaymentConstraints = Just (PaymentConstraints 747969 1500)
- , bhdAllowedFeatures = Just BS.empty
+rb_carol_data :: IO BlindedHopData
+rb_carol_data = do
+ s <- scid 0 0 1105
+ m <- msat 1500
+ override <- hex_point V.rbDavePathKey
+ pure empty_blinded_hop_data
+ { bhd_short_channel_id = Just s
+ , bhd_next_path_key_override = Just override
+ , bhd_payment_relay = Just (PaymentRelay 48 100 500)
+ , bhd_payment_constraints = Just (PaymentConstraints 747969 m)
+ , bhd_allowed_features = empty_features
}
-rbDaveData :: IO BlindedHopData
-rbDaveData = do
- scid <- assertScid 0 0 561
- pure emptyHopData
- { bhdPadding = Just (BS.replicate 35 0)
- , bhdShortChannelId = Just scid
- , bhdPaymentRelay = Just (PaymentRelay 144 250 0)
- , bhdPaymentConstraints = Just (PaymentConstraints 747921 1500)
- , bhdAllowedFeatures = Just BS.empty
+rb_dave_data :: IO BlindedHopData
+rb_dave_data = do
+ s <- scid 0 0 561
+ m <- msat 1500
+ pure empty_blinded_hop_data
+ { bhd_padding = Just (BS.replicate 35 0)
+ , bhd_short_channel_id = Just s
+ , bhd_payment_relay = Just (PaymentRelay 144 250 0)
+ , bhd_payment_constraints = Just (PaymentConstraints 747921 m)
+ , bhd_allowed_features = empty_features
}
-rbEveData :: IO BlindedHopData
-rbEveData = pure emptyHopData
- { bhdPadding = Just (BS.replicate 26 0)
- , bhdPathId = Just (BS.pack [0xde, 0xad, 0xbe, 0xef])
- , bhdPaymentConstraints = Just (PaymentConstraints 747777 1500)
- , bhdAllowedFeatures = Just (BS.cons 0x02 (BS.replicate 14 0))
- }
+rb_eve_data :: IO BlindedHopData
+rb_eve_data = do
+ m <- msat 1500
+ ex <- tlvs [BOLT1.TlvRecord 65535 "\ACK\193"]
+ pure empty_blinded_hop_data
+ { bhd_padding = Just (BS.replicate 26 0)
+ , bhd_path_id = Just "\222\173\190\239"
+ , bhd_payment_constraints = Just (PaymentConstraints 747777 m)
+ , bhd_allowed_features =
+ Just (BOLT9.parse (BS.cons 0x02 (BS.replicate 14 0)))
+ , bhd_extra = ex
+ }
--- | A hop of route-blinding-test.json. 'rbUnknown' holds the unknown
--- odd TLVs that the hop's data carries in addition to 'rbData'.
+-- a hop of route-blinding-test.json
data RbHop = RbHop
- { rbData :: !BlindedHopData
- , rbUnknown :: ![TlvRecord]
- , rbNodeId :: !Secp256k1.Projective
- , rbTlvs :: !BS.ByteString
- , rbPathPrivKey :: !BS.ByteString
- , rbPathKey :: !Secp256k1.Projective
- , rbSharedSecret :: !BS.ByteString
- , rbRho :: !BS.ByteString
- , rbEncrypted :: !BS.ByteString
- , rbBlindedNodeId :: !BS.ByteString
- , rbNodeKey :: !BS.ByteString
- , rbBlindedKey :: !BS.ByteString
- , rbNextPathKey :: !Secp256k1.Projective
+ { rb_data :: !BlindedHopData
+ , rb_node_id :: !BOLT1.Point
+ , rb_tlvs :: !BS.ByteString
+ , rb_path_key :: !BOLT1.Point
+ , rb_encrypted :: !BS.ByteString
+ , rb_blinded_id :: !BOLT1.Point
+ , rb_node_key :: !SecretKey
+ , rb_next_path_key :: !BOLT1.Point
}
-rbHops :: IO (RbHop, RbHop, RbHop, RbHop)
-rbHops = do
- bob <- hop rbBobData [TlvRecord 561 (BS.pack [0x12, 0x34, 0x56])]
- ( V.rbBobNodeId, V.rbBobTlvs, V.rbBobPathPrivKey, V.rbBobPathKey
- , V.rbBobSharedSecret, V.rbBobRho, V.rbBobEncryptedData
- , V.rbBobBlindedNodeId )
- (V.rbBobNodeKey, V.rbBobBlindedPrivKey, V.rbBobNextPathKey)
- carol <- hop rbCarolData []
- ( V.rbCarolNodeId, V.rbCarolTlvs, V.rbCarolPathPrivKey
- , V.rbCarolPathKey, V.rbCarolSharedSecret, V.rbCarolRho
- , V.rbCarolEncryptedData, V.rbCarolBlindedNodeId )
- (V.rbCarolNodeKey, V.rbCarolBlindedPrivKey, V.rbCarolNextPathKey)
- dave <- hop rbDaveData []
- ( V.rbDaveNodeId, V.rbDaveTlvs, V.rbDavePathPrivKey, V.rbDavePathKey
- , V.rbDaveSharedSecret, V.rbDaveRho, V.rbDaveEncryptedData
- , V.rbDaveBlindedNodeId )
- (V.rbDaveNodeKey, V.rbDaveBlindedPrivKey, V.rbDaveNextPathKey)
- eve <- hop rbEveData [TlvRecord 65535 (BS.pack [0x06, 0xc1])]
- ( V.rbEveNodeId, V.rbEveTlvs, V.rbEvePathPrivKey, V.rbEvePathKey
- , V.rbEveSharedSecret, V.rbEveRho, V.rbEveEncryptedData
- , V.rbEveBlindedNodeId )
- (V.rbEveNodeKey, V.rbEveBlindedPrivKey, V.rbEveNextPathKey)
- pure (bob, carol, dave, eve)
+rb_hops :: IO [RbHop]
+rb_hops = sequence
+ [ hop rb_bob_data
+ ( V.rbBobNodeId, V.rbBobTlvs, V.rbBobPathKey
+ , V.rbBobEncryptedData, V.rbBobBlindedNodeId )
+ (V.rbBobNodeKey, V.rbBobNextPathKey)
+ , hop rb_carol_data
+ ( V.rbCarolNodeId, V.rbCarolTlvs, V.rbCarolPathKey
+ , V.rbCarolEncryptedData, V.rbCarolBlindedNodeId )
+ (V.rbCarolNodeKey, V.rbCarolNextPathKey)
+ , hop rb_dave_data
+ ( V.rbDaveNodeId, V.rbDaveTlvs, V.rbDavePathKey
+ , V.rbDaveEncryptedData, V.rbDaveBlindedNodeId )
+ (V.rbDaveNodeKey, V.rbDaveNextPathKey)
+ , hop rb_eve_data
+ ( V.rbEveNodeId, V.rbEveTlvs, V.rbEvePathKey
+ , V.rbEveEncryptedData, V.rbEveBlindedNodeId )
+ (V.rbEveNodeKey, V.rbEveNextPathKey)
+ ]
where
- hop mkData unknown (nid, tlvs, ppk, pk, ss, rho, enc, bid)
- (nk, bk, npk) =
- RbHop <$> mkData <*> pure unknown <*> unhexPoint nid
- <*> unhex tlvs <*> unhex ppk <*> unhexPoint pk <*> unhex ss
- <*> unhex rho <*> unhex enc <*> unhex bid <*> unhex nk
- <*> unhex bk <*> unhexPoint npk
-
-routeBlindingTests :: TestTree
-routeBlindingTests = testGroup "route-blinding-test.json" [
- testCase "encrypted_data_tlv encoding" $ do
- (bob, carol, dave, eve) <- rbHops
- mapM_ (\h -> do
- -- unknown odd TLVs are the last records in the vectors
- encodeBlindedHopData (rbData h) <> encodeTlvStream (rbUnknown h)
- @?= rbTlvs h
- decodeBlindedHopData (rbTlvs h) @?= Just (rbData h))
- [bob, carol, dave, eve]
-
- , testCase "per-hop shared secret and rho" $ do
- (bob, carol, dave, eve) <- rbHops
- mapM_ (\h -> do
- ss <- demand "computeSharedSecret" $
- computeSharedSecret (rbPathPrivKey h) (rbNodeId h)
- ss @?= SharedSecret (rbSharedSecret h)
- deriveBlindingRho ss @?= DerivedKey (rbRho h)
- deriveBlindedNodeId ss (rbNodeId h)
- @?= Just (rbBlindedNodeId h))
- [bob, carol, dave, eve]
-
- , testCase "nextEphemeral yields the next path key pair" $ do
- (bob, carol, dave, eve) <- rbHops
- mapM_ (\(h, nxt) -> do
- let ss = SharedSecret (rbSharedSecret h)
- nextEphemeral (rbPathPrivKey h) (rbPathKey h) ss
- @?= Just (rbPathPrivKey nxt, rbPathKey nxt))
- [(bob, carol), (dave, eve)]
-
- , testCase "createBlindedPath (Bob -> Carol)" $ do
- (bob, carol, _, _) <- rbHops
- path <- either (assertFailure . show) pure $
- createBlindedPath (rbPathPrivKey bob)
- [(rbNodeId bob, rbData bob), (rbNodeId carol, rbData carol)]
- bpIntroductionNode path @?= rbNodeId bob
- bpBlindingKey path @?= rbPathKey bob
- case bpBlindedHops path of
- [b, c] -> do
- bhBlindedNodeId b @?= rbBlindedNodeId bob
- bhBlindedNodeId c @?= rbBlindedNodeId carol
- -- Bob's data in the vector carries an unknown TLV (561)
- -- that BlindedHopData cannot represent, so only Carol's
- -- encrypted_data can be reproduced.
- bhEncryptedData c @?= rbEncrypted carol
- _ -> assertFailure "expected 2 blinded hops"
-
- , testCase "createBlindedPath (Dave -> Eve)" $ do
- (_, _, dave, eve) <- rbHops
- path <- either (assertFailure . show) pure $
- createBlindedPath (rbPathPrivKey dave)
- [(rbNodeId dave, rbData dave), (rbNodeId eve, rbData eve)]
- bpIntroductionNode path @?= rbNodeId dave
- bpBlindingKey path @?= rbPathKey dave
- case bpBlindedHops path of
- [d, e] -> do
- bhBlindedNodeId d @?= rbBlindedNodeId dave
- bhBlindedNodeId e @?= rbBlindedNodeId eve
- -- Eve's data carries an unknown TLV (65535); see above.
- bhEncryptedData d @?= rbEncrypted dave
- _ -> assertFailure "expected 2 blinded hops"
-
- , testCase "unblinding at each hop" $ do
- (bob, carol, dave, eve) <- rbHops
- mapM_ (\h -> do
- case processBlindedHop (rbNodeKey h) (rbPathKey h)
- (rbEncrypted h) of
- Left e -> assertFailure ("processBlindedHop: " ++ show e)
- Right (dat, npk) -> do
- dat @?= rbData h
- npk @?= rbNextPathKey h
- blindedPrivKey (rbNodeKey h) (rbPathKey h)
- @?= Just (rbBlindedKey h))
- [bob, carol, dave, eve]
+ hop mk (nid, tl, pk, enc, bid) (nk, npk) =
+ RbHop <$> mk <*> hex_point nid <*> unhex tl <*> hex_point pk
+ <*> unhex enc <*> hex_point bid <*> hex_key nk
+ <*> hex_point npk
+
+route_blinding_vectors :: TestTree
+route_blinding_vectors = testGroup "route-blinding-test.json" [
+ testCase "encrypted_data_tlv encodings" $ do
+ hops <- rb_hops
+ forM_ hops $ \h -> do
+ encode_blinded_hop_data (rb_data h) @?= Right (rb_tlvs h)
+ decode_blinded_hop_data (rb_tlvs h) @?= Right (rb_data h)
+ , testCase "create_blinded_path reproduces the route" $ do
+ hops <- rb_hops
+ (bob, carol, dave, eve) <- case hops of
+ [b, c, d, e] -> pure (b, c, d, e)
+ _ -> assertFailure "expected 4 hops"
+ -- Bob creates Bob -> Carol, Eve creates Dave -> Eve
+ bob_seed <- hex_key V.rbBobPathPrivKey
+ dave_seed <- hex_key V.rbDavePathPrivKey
+ let check seed hs = do
+ path <- right "create_blinded_path" $ create_blinded_path seed
+ [ (rb_node_id h, rb_data h) | h <- hs ]
+ case hs of
+ h : _ -> do
+ bp_first_node_id path @?= rb_node_id h
+ bp_first_path_key path @?= rb_path_key h
+ [] -> assertFailure "no hops"
+ bp_hops path @?=
+ [ BlindedHop (rb_blinded_id h) (rb_encrypted h) | h <- hs ]
+ check bob_seed [bob, carol]
+ check dave_seed [dave, eve]
+ , testCase "each hop decrypts its data and next path key" $ do
+ hops <- rb_hops
+ forM_ hops $ \h -> do
+ info <- right "decrypt_recipient_data" $
+ decrypt_recipient_data (rb_node_key h) (rb_path_key h)
+ (rb_encrypted h)
+ bi_data info @?= rb_data h
+ encode_blinded_hop_data (bi_data info) @?= Right (rb_tlvs h)
+ -- Carol's next path key is her next_path_key_override
+ bi_next_path_key info @?= rb_next_path_key h
+ , testCase "each hop decrypts an onion to its blinded id" $ do
+ -- the onion is encrypted to blinded_node_id, so processing it
+ -- requires the blinded private key of the vector
+ hops <- rb_hops
+ session <- key 0x55
+ forM_ hops $ \h -> do
+ let pl = empty_hop_payload
+ { hp_encrypted_data = Just (rb_encrypted h) }
+ (pkt, sss) <- right "construct" $
+ construct session [Hop (rb_blinded_id h) pl] ad
+ r <- receive (process (rb_node_key h) pkt ad (Just (rb_path_key h)))
+ Just (rcv_shared_secret r) @?= safe_head sss
+ fmap bi_next_path_key (rcv_blinded r) @?= Just (rb_next_path_key h)
]
--- Spec vectors: blinded-payment-onion-test.json ----------------------------
-
-bpBobData :: IO BlindedHopData
-bpBobData = do
- scid <- assertScid 0 0 1
- pure emptyHopData
- { bhdPadding = Just (BS.replicate 32 0)
- , bhdShortChannelId = Just scid
- , bhdPaymentRelay = Just (PaymentRelay 50 0 10000)
- , bhdPaymentConstraints = Just (PaymentConstraints 750150 50)
- , bhdAllowedFeatures = Just BS.empty
+-- spec vectors: blinded-payment-onion-test.json ------------------------------
+
+bp_bob_data :: IO BlindedHopData
+bp_bob_data = do
+ s <- scid 0 0 1
+ m <- msat 50
+ pure empty_blinded_hop_data
+ { bhd_padding = Just (BS.replicate 32 0)
+ , bhd_short_channel_id = Just s
+ , bhd_payment_relay = Just (PaymentRelay 50 0 10000)
+ , bhd_payment_constraints = Just (PaymentConstraints 750150 m)
+ , bhd_allowed_features = empty_features
}
-bpCarolData :: IO BlindedHopData
-bpCarolData = do
- scid <- assertScid 0 0 2
- override <- unhex V.rbDavePathKey
- pure emptyHopData
- { bhdShortChannelId = Just scid
- , bhdNextPathKeyOverride = Just override
- , bhdPaymentRelay = Just (PaymentRelay 75 150 100)
- , bhdPaymentConstraints = Just (PaymentConstraints 750100 50)
- , bhdAllowedFeatures = Just BS.empty
+bp_carol_data :: IO BlindedHopData
+bp_carol_data = do
+ s <- scid 0 0 2
+ m <- msat 50
+ override <- hex_point V.rbDavePathKey
+ pure empty_blinded_hop_data
+ { bhd_short_channel_id = Just s
+ , bhd_next_path_key_override = Just override
+ , bhd_payment_relay = Just (PaymentRelay 75 150 100)
+ , bhd_payment_constraints = Just (PaymentConstraints 750100 m)
+ , bhd_allowed_features = empty_features
}
-bpDaveData :: IO BlindedHopData
-bpDaveData = do
- scid <- assertScid 0 0 3
- pure emptyHopData
- { bhdPadding = Just (BS.replicate 34 0)
- , bhdShortChannelId = Just scid
- , bhdPaymentRelay = Just (PaymentRelay 25 100 0)
- , bhdPaymentConstraints = Just (PaymentConstraints 750025 50)
- , bhdAllowedFeatures = Just BS.empty
+bp_dave_data :: IO BlindedHopData
+bp_dave_data = do
+ s <- scid 0 0 3
+ m <- msat 50
+ pure empty_blinded_hop_data
+ { bhd_padding = Just (BS.replicate 34 0)
+ , bhd_short_channel_id = Just s
+ , bhd_payment_relay = Just (PaymentRelay 25 100 0)
+ , bhd_payment_constraints = Just (PaymentConstraints 750025 m)
+ , bhd_allowed_features = empty_features
}
-bpEveData :: IO BlindedHopData
-bpEveData = do
- pathId <- unhex "c9cf92f45ade68345bc20ae672e2012f4af487ed4415"
- pure emptyHopData
- { bhdPadding = Just (BS.replicate 28 0)
- , bhdPathId = Just pathId
- , bhdPaymentConstraints = Just (PaymentConstraints 750000 50)
- , bhdAllowedFeatures = Just BS.empty
+bp_eve_data :: IO BlindedHopData
+bp_eve_data = do
+ m <- msat 50
+ path_id <- unhex "c9cf92f45ade68345bc20ae672e2012f4af487ed4415"
+ pure empty_blinded_hop_data
+ { bhd_padding = Just (BS.replicate 28 0)
+ , bhd_path_id = Just path_id
+ , bhd_payment_constraints = Just (PaymentConstraints 750000 m)
+ , bhd_allowed_features = empty_features
}
--- | Per hop: (key the onion is encrypted to, onion received, payload
--- without length prefix).
-bpRoute :: IO [(BS.ByteString, Secp256k1.Projective, BS.ByteString,
- BS.ByteString)]
-bpRoute = do
- alice <- unhex V.onionNodeKey0
- bob <- unhex V.rbBobNodeKey
- -- Carol, Dave and Eve decrypt with their blinded private keys
- carol <- unhex V.rbCarolBlindedPrivKey
- dave <- unhex V.rbDaveBlindedPrivKey
- eve <- unhex V.rbEveBlindedPrivKey
- mapM (\(k, pub, onion, pl) -> do
- p <- unhexPoint pub
- o <- unhex onion
- body <- unhex pl >>= unprefix
- pure (k, p, o, body))
- [ (alice, V.bpAlicePubKey, V.bpAliceOnion, V.bpAlicePayload)
- , (bob, V.bpBobPubKey, V.bpBobOnion, V.bpBobPayload)
- , (carol, V.bpCarolPubKey, V.bpCarolOnion, V.bpCarolPayload)
- , (dave, V.bpDavePubKey, V.bpDaveOnion, V.bpDavePayload)
- , (eve, V.bpEvePubKey, V.bpEveOnion, V.bpEvePayload)
+-- per hop: node key, public key the onion is encrypted to, onion
+-- received, payload (without its length prefix)
+bp_route
+ :: IO [(SecretKey, BOLT1.Point, BS.ByteString, BS.ByteString)]
+bp_route = mapM hop
+ [ (V.onionNodeKey0, V.bpAlicePubKey, V.bpAliceOnion, V.bpAlicePayload)
+ , (V.rbBobNodeKey, V.bpBobPubKey, V.bpBobOnion, V.bpBobPayload)
+ , (V.rbCarolNodeKey, V.bpCarolPubKey, V.bpCarolOnion, V.bpCarolPayload)
+ , (V.rbDaveNodeKey, V.bpDavePubKey, V.bpDaveOnion, V.bpDavePayload)
+ , (V.rbEveNodeKey, V.bpEvePubKey, V.bpEveOnion, V.bpEvePayload)
]
-
-blindedPaymentTests :: TestTree
-blindedPaymentTests = testGroup "blinded-payment-onion-test.json" [
- testCase "construct reproduces the spec onion" $ do
- route <- bpRoute
- sk <- unhex V.bpSessionKey
- ad <- unhex V.bpAssocData
+ where
+ hop (k, p, o, pl) = (,,,) <$> hex_key k <*> hex_point p <*> unhex o
+ <*> (unhex pl >>= unprefix)
+
+blinded_payment_vectors :: TestTree
+blinded_payment_vectors = testGroup "blinded-payment-onion-test.json" [
+ testCase "construct reproduces the onion" $ do
+ route <- bp_route
+ sk <- hex_key V.bpSessionKey
+ assoc <- unhex V.bpAssocData
+ expected <- unhex V.bpAliceOnion
hops <- mapM (\(_, pub, _, body) -> do
- hp <- demand "decodeHopPayload" (decodeHopPayload body)
- encodeHopPayload hp @?= body
+ hp <- right "decode_hop_payload" (decode_hop_payload body)
+ encode_hop_payload hp @?= Right body
pure (Hop pub hp)) route
- expected <- unhex V.bpAliceOnion
- case construct sk hops ad of
- Left e -> assertFailure ("construct failed: " ++ show e)
- Right (pkt, _) -> encodeOnionPacket pkt @?= expected
-
- , testCase "each hop forwards the spec onion" $ do
- route <- bpRoute
- ad <- unhex V.bpAssocData
- let go [] = assertFailure "no hops"
- go ((k, _, onion, body) : rest) = do
- pkt <- demand "decodeOnionPacket" (decodeOnionPacket onion)
- case (process k pkt ad, rest) of
- (Left e, _) -> assertFailure ("process failed: " ++ show e)
- (Right (Forward fi), (_, _, next, _) : _) -> do
- encodeHopPayload (fiPayload fi) @?= body
- encodeOnionPacket (fiNextPacket fi) @?= next
- go rest
- (Right (Receive ri), []) ->
- encodeHopPayload (riPayload ri) @?= body
- (Right _, _) -> assertFailure "unexpected result"
- go route
-
- , testCase "blinded hops decrypt their data and next path key" $ do
- bobKey <- unhex V.rbBobNodeKey
- carolKey <- unhex V.rbCarolNodeKey
- daveKey <- unhex V.rbDaveNodeKey
- eveKey <- unhex V.rbEveNodeKey
- firstPathKey <- unhexPoint V.rbBobPathKey
- bobNext <- unhexPoint V.bpBobNextPathKey
- carolNext <- unhexPoint V.bpCarolNextPathKey
- daveNext <- unhexPoint V.bpDaveNextPathKey
- eveNext <- unhexPoint V.bpEveNextPathKey
- bobBody <- unhex V.bpBobPayload >>= unprefix
- bobHp <- demand "decodeHopPayload" (decodeHopPayload bobBody)
- hpCurrentPathKey bobHp @?= Just (Secp256k1.serialize_point firstPathKey)
- let encOf pl = do
- body <- unhex pl >>= unprefix
- hp <- demand "decodeHopPayload" (decodeHopPayload body)
- demand "encrypted_recipient_data" (hpEncryptedData hp)
- bobData <- bpBobData
- carolData <- bpCarolData
- daveData <- bpDaveData
- eveData <- bpEveData
- hops <- sequence
- [ (,,,,) bobKey firstPathKey <$> encOf V.bpBobPayload
- <*> pure bobData <*> pure bobNext
- , (,,,,) carolKey bobNext <$> encOf V.bpCarolPayload
- <*> pure carolData <*> pure carolNext
- , (,,,,) daveKey carolNext <$> encOf V.bpDavePayload
- <*> pure daveData <*> pure daveNext
- , (,,,,) eveKey daveNext <$> encOf V.bpEvePayload
- <*> pure eveData <*> pure eveNext
- ]
- mapM_ (\(k, pk, enc, dat, npk) ->
- processBlindedHop k pk enc @?= Right (dat, npk)) hops
+ (pkt, _) <- right "construct" (construct sk hops assoc)
+ encode_onion_packet pkt @?= expected
+ , testCase "the route's payloads" $ do
+ route <- bp_route
+ first_pk <- hex_point V.rbBobPathKey
+ s <- scid 0 0 10
+ (a0, a1, a2) <- three =<< mapM msat [110125, 100000, 150000]
+ payloads <- mapM (\(_, _, _, body) ->
+ right "decode_hop_payload" (decode_hop_payload body)) route
+ case payloads of
+ [alice, bob, carol, dave, eve] -> do
+ alice @?= empty_hop_payload
+ { hp_short_channel_id = Just s, hp_amt_to_forward = Just a0
+ , hp_outgoing_cltv_value = Just 749150 }
+ hp_current_path_key bob @?= Just first_pk
+ forM_ [bob, carol, dave] $ \hp -> do
+ assertBool "encrypted data" (hp_encrypted_data hp /= Nothing)
+ hp { hp_encrypted_data = Nothing
+ , hp_current_path_key = Nothing } @?= empty_hop_payload
+ eve { hp_encrypted_data = Nothing } @?= empty_hop_payload
+ { hp_amt_to_forward = Just a1, hp_total_amount_msat = Just a2
+ , hp_outgoing_cltv_value = Just 749000 }
+ _ -> assertFailure "expected 5 payloads"
+ , testCase "each hop processes its onion" $ do
+ route <- bp_route
+ assoc <- unhex V.bpAssocData
+ datas <- sequence [bp_bob_data, bp_carol_data, bp_dave_data,
+ bp_eve_data]
+ nexts <- mapM hex_point
+ [ V.bpBobNextPathKey, V.bpCarolNextPathKey, V.bpDaveNextPathKey
+ , V.bpEveNextPathKey ]
+ -- (hop, path_key received, expected blinded info)
+ let expect = zip3 route
+ (Nothing : Nothing : map Just (take 3 nexts))
+ (Nothing : map Just (zipWith BlindedInfo datas nexts))
+ go [] = assertFailure "no hops"
+ go [((k, _, onion, body), mpk, binfo)] = do
+ pkt <- right "decode" (decode_onion_packet onion)
+ r <- receive (process k pkt assoc mpk)
+ encode_hop_payload (rcv_payload r) @?= Right body
+ rcv_blinded r @?= binfo
+ go (((k, _, onion, body), mpk, binfo) : rest) = do
+ pkt <- right "decode" (decode_onion_packet onion)
+ f <- forward (process k pkt assoc mpk)
+ encode_hop_payload (fwd_payload f) @?= Right body
+ fwd_blinded f @?= binfo
+ forM_ (take 1 rest) $ \((_, _, next, _), _, _) ->
+ encode_onion_packet (fwd_next_packet f) @?= next
+ go rest
+ go expect
+ ]
+
+-- properties -----------------------------------------------------------------
+
+properties :: TestTree
+properties = testGroup "properties" [
+ testProperty "onion packets round-trip" $
+ forAll gen_onion $ \pkt ->
+ decode_onion_packet (encode_onion_packet pkt) === Right pkt
+ , testProperty "hop payloads round-trip" $
+ forAll gen_hop_payload $ \hp ->
+ fmap decode_hop_payload (encode_hop_payload hp) === Right (Right hp)
+ , testProperty "blinded hop data round-trips" $
+ forAll gen_blinded_hop_data $ \d ->
+ fmap decode_blinded_hop_data (encode_blinded_hop_data d)
+ === Right (Right d)
+ , testProperty "failure messages round-trip" $
+ forAll gen_failure_message $ \fm ->
+ fmap decode_failure_message (encode_failure_message fm)
+ === Right (Right fm)
+ , testProperty "wrap_error is an involution" $
+ forAll gen_secret $ \ss ->
+ forAll (gen_bytes 0 600) $ \bs ->
+ wrap_error ss (wrap_error ss (ErrorPacket bs)) === ErrorPacket bs
+ , testProperty "errors are attributed to the failing hop" $
+ forAll (choose (1, 20)) $ \n ->
+ forAll (vectorOf n gen_secret) $ \sss ->
+ forAll (choose (0, n - 1)) $ \i ->
+ forAll gen_failure_message $ \fm ->
+ case drop i sss of
+ ss : _ | Right pkt <- construct_error ss fm ->
+ unwrap_error sss (foldr wrap_error pkt (take i sss))
+ === Attributed i fm
+ _ -> property False
+ , testProperty "routes of 1 to 20 hops construct and process" $
+ forAll (choose (1, 20)) $ \n ->
+ forAll gen_key $ \sk ->
+ forAll (vectorOf n gen_key) $ \keys ->
+ forAll (vectorOf n (gen_route_payload n)) $ \pls ->
+ route_roundtrip sk (zip keys pls)
+ ]
+
+-- construct an onion over a route, then process it at every hop
+route_roundtrip :: SecretKey -> [(SecretKey, HopPayload)] -> Property
+route_roundtrip sk route =
+ case construct sk [Hop (public_key k) hp | (k, hp) <- route] ad of
+ Left e -> counterexample (show e) False
+ Right (pkt, sss) -> length sss === length route .&&. go route sss pkt
+ where
+ go [(k, hp)] [ss] pkt = case process k pkt ad Nothing of
+ Right (Receive r) ->
+ rcv_payload r === hp .&&. property (rcv_shared_secret r == ss)
+ other -> counterexample (show other) False
+ go ((k, hp) : rest) (ss : sss) pkt = case process k pkt ad Nothing of
+ Right (Forward f) ->
+ fwd_payload f === hp
+ .&&. property (fwd_shared_secret f == ss)
+ .&&. go rest sss (fwd_next_packet f)
+ other -> counterexample (show other) False
+ go _ _ _ = property False
+
+-- generators -----------------------------------------------------------------
+
+gen_bytes :: Int -> Int -> Gen BS.ByteString
+gen_bytes lo hi = do
+ n <- choose (lo, hi)
+ BS.pack <$> vectorOf n arbitrary
+
+gen_key :: Gen SecretKey
+gen_key = gen_bytes 32 32 `suchThatMap` secret_key
+
+gen_secret :: Gen SharedSecret
+gen_secret = gen_bytes 32 32 `suchThatMap` shared_secret
+
+gen_point :: Gen BOLT1.Point
+gen_point = do
+ prefix <- elements [0x02, 0x03]
+ x <- gen_bytes 32 32
+ pure (BS.cons prefix x) `suchThatMap` BOLT1.point
+
+gen_msat :: Gen BOLT1.MilliSatoshi
+gen_msat = oneof
+ [ choose (0, 0xffff), choose (0, BOLT1.un_milli_satoshi
+ BOLT1.max_milli_satoshi) ]
+ `suchThatMap` BOLT1.milli_satoshi
+
+gen_scid :: Gen BOLT1.ShortChannelId
+gen_scid = BOLT1.ShortChannelId <$> arbitrary
+
+gen_maybe :: Gen a -> Gen (Maybe a)
+gen_maybe g = oneof [pure Nothing, Just <$> g]
+
+-- extra records with odd types not in the given list
+gen_extra :: [Word64] -> Gen BOLT1.TlvStream
+gen_extra known = do
+ n <- choose (0, 3)
+ ts <- vectorOf n (oneof [choose (1, 300), choose (65536, 70000)])
+ recs <- mapM (\t -> BOLT1.TlvRecord t <$> gen_bytes 0 20)
+ [ t | t <- ts, odd t, t `notElem` known ]
+ let uniq = nubBy (\a b -> BOLT1.tlv_type a == BOLT1.tlv_type b) recs
+ pure uniq `suchThatMap` BOLT1.tlv_stream
+
+gen_onion :: Gen OnionPacket
+gen_onion = OnionPacket
+ <$> arbitrary
+ <*> gen_point
+ <*> (gen_bytes 1300 1300 `suchThatMap` hop_payloads)
+ <*> (gen_bytes 32 32 `suchThatMap` hmac32)
+
+gen_hop_payload :: Gen HopPayload
+gen_hop_payload = HopPayload
+ <$> gen_maybe gen_msat
+ <*> gen_maybe arbitrary
+ <*> gen_maybe gen_scid
+ <*> gen_maybe (PaymentData
+ <$> (gen_bytes 32 32 `suchThatMap` payment_secret)
+ <*> gen_msat)
+ <*> gen_maybe (gen_bytes 0 50)
+ <*> gen_maybe gen_point
+ <*> gen_maybe (gen_bytes 0 50)
+ <*> gen_maybe gen_msat
+ <*> gen_extra []
+
+gen_blinded_hop_data :: Gen BlindedHopData
+gen_blinded_hop_data = BlindedHopData
+ <$> gen_maybe (gen_bytes 0 40)
+ <*> gen_maybe gen_scid
+ <*> gen_maybe gen_point
+ <*> gen_maybe (gen_bytes 0 40)
+ <*> gen_maybe gen_point
+ <*> gen_maybe (PaymentRelay <$> arbitrary <*> arbitrary <*> arbitrary)
+ <*> gen_maybe (PaymentConstraints <$> arbitrary <*> gen_msat)
+ <*> gen_maybe (BOLT9.parse <$> gen_bytes 0 8)
+ <*> gen_extra [1]
+
+gen_failure_message :: Gen FailureMessage
+gen_failure_message = do
+ f <- gen_failure
+ ex <- case f of
+ -- these take every byte after the code, or may be ambiguous with a
+ -- TLV stream
+ UnknownFailure _ _ -> pure no_tlvs
+ InvalidOnionPayload Nothing -> pure no_tlvs
+ _ -> gen_extra []
+ pure (FailureMessage f ex)
+
+gen_failure :: Gen Failure
+gen_failure = oneof
+ [ pure TemporaryNodeFailure
+ , pure PermanentNodeFailure
+ , pure RequiredNodeFeatureMissing
+ , InvalidOnionVersion <$> hash
+ , InvalidOnionHmac <$> hash
+ , InvalidOnionKey <$> hash
+ , TemporaryChannelFailure <$> update
+ , pure PermanentChannelFailure
+ , pure RequiredChannelFeatureMissing
+ , pure UnknownNextPeer
+ , AmountBelowMinimum <$> gen_msat <*> update
+ , FeeInsufficient <$> gen_msat <*> update
+ , IncorrectCltvExpiry <$> arbitrary <*> update
+ , ExpiryTooSoon <$> update
+ , IncorrectOrUnknownPaymentDetails <$> gen_msat <*> arbitrary
+ , FinalIncorrectCltvExpiry <$> arbitrary
+ , FinalIncorrectHtlcAmount <$> gen_msat
+ , ChannelDisabled <$> arbitrary <*> update
+ , pure ExpiryTooFar
+ , InvalidOnionPayload <$> gen_maybe ((,) <$> arbitrary <*> arbitrary)
+ , pure MppTimeout
+ , InvalidOnionBlinding <$> hash
+ , UnknownFailure <$> (arbitrary `suchThat` unknown) <*> gen_bytes 0 20
]
+ where
+ hash = gen_bytes 32 32 `suchThatMap` onion_hash
+ update = gen_bytes 0 100
+ unknown c = c `notElem`
+ [ 0x2002, 0x6002, 0x6003, 0xc004, 0xc005, 0xc006, 0x1007, 0x4008
+ , 0x4009, 0x400a, 0x100b, 0x100c, 0x100d, 0x100e, 0x400f, 0x0012
+ , 0x0013, 0x1014, 0x0015, 0x4016, 0x0017, 0xc018 ]
+
+-- a payload whose shift size fits n hops in 1300 bytes
+--
+-- The typed fields take at most 26 bytes; an odd record of type 65537
+-- adds at most 8 bytes of framing plus its value. With a 3-byte length
+-- prefix and 32-byte HMAC a hop then shifts at most 69 bytes plus the
+-- value's length.
+gen_route_payload :: Int -> Gen HopPayload
+gen_route_payload n = do
+ amt <- gen_msat
+ cltv <- arbitrary
+ s <- gen_maybe gen_scid
+ let budget = 1300 `div` n - 69
+ ex <- if budget < 0
+ then pure no_tlvs
+ else do
+ v <- gen_bytes 0 budget
+ pure (BOLT1.TlvRecord 65537 v : [])
+ `suchThatMap` BOLT1.tlv_stream
+ pure empty_hop_payload
+ { hp_amt_to_forward = Just amt
+ , hp_outgoing_cltv_value = Just cltv
+ , hp_short_channel_id = s
+ , hp_extra = ex
+ }