haskoin-node-2.5.0: test/unit/Haskoin/Node/PeerMgr/AddressesSpec.hs
module Haskoin.Node.PeerMgr.AddressesSpec (spec) where
import Data.List (nub, sort)
import Data.Maybe (isJust, isNothing, mapMaybe)
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime)
import Data.Word (Word64)
import Haskoin.Node.PeerMgr.Addresses
import Network.Socket (SockAddr (..), tupleToHostAddress)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
t0 :: UTCTime
t0 = UTCTime (fromGregorian 2026 1 1) 0
later :: NominalDiffTime -> UTCTime
later d = d `addUTCTime` t0
book :: [Int] -> AddressBook Int
book = foldl' (flip (addAddress Discovered)) emptyBook
-- | Take an address, with any address allowed.
takeAny :: (Ord a) => UTCTime -> AddressBook a -> Maybe (a, AddressBook a)
takeAny now = takeAddress now 0 (const True)
-- | Take an address that must be available.
taken :: UTCTime -> AddressBook Int -> (Int, AddressBook Int)
taken now b = case takeAny now b of
Just x -> x
Nothing -> error "no address to take"
-- | Take a given address, end its connection, and return the new book. Only
-- that address may be taken.
cycleWith :: Outcome -> Int -> AddressBook Int -> AddressBook Int
cycleWith outcome a b = case takeAddress t0 0 (== a) b of
Just (a', b') | a == a' -> finishAddress t0 outcome a b'
_ -> error "address not taken"
-- | Every address that some random number makes 'takeAddress' pick, sorted.
picks :: (Ord a) => (a -> Bool) -> AddressBook a -> [a]
picks allowed b =
sort (nub (mapMaybe (fmap fst . (\r -> takeAddress t0 r allowed b)) [0 .. 99]))
-- | When an address can be tried again, if it is waiting.
retryAt :: Int -> AddressBook Int -> Maybe UTCTime
retryAt a b = case addressState a b of
Just (Waiting t _) -> Just t
_ -> Nothing
-- | Transitions applied to a few addresses at increasing times.
data Op
= Add Source Int
| Take NominalDiffTime Word64
| Give Int
| Connected Int
| Finish NominalDiffTime Outcome Int
deriving stock (Show)
instance Arbitrary Op where
arbitrary =
oneof
[ Add <$> elements [Configured, Seeded, Discovered] <*> addr,
Take <$> time <*> arbitrary,
Give <$> addr,
Connected <$> addr,
Finish <$> time <*> elements [Disconnected, Failed, Misbehaved] <*> addr
]
where
addr = chooseInt (1, 4)
time = fromIntegral <$> chooseInt (0, 100000)
apply :: AddressBook Int -> Op -> AddressBook Int
apply b = \case
Add src a -> addAddress src a b
Take d r -> maybe b snd (takeAddress (later d) r (const True) b)
Give a -> returnAddress a b
Connected a -> markConnected a b
Finish d o a -> finishAddress (later d) o a b
spec :: Spec
spec = describe "address book" $ do
it "hands out each address once" $ do
let (a, b) = taken t0 (book [1])
a `shouldBe` 1
addressState 1 b `shouldBe` Just InUse
takeAny t0 b `shouldSatisfy` isNothing
it "takes back an address that was not used" $ do
let (_, b) = taken t0 (book [1])
addressState 1 (returnAddress 1 b) `shouldBe` Just Ready
it "waits briefly later a normal disconnection" $ do
let b = cycleWith Disconnected 1 (book [1])
retryAt 1 b `shouldBe` Just (later retryDelay)
takeAny t0 b `shouldSatisfy` isNothing
takeAny (later retryDelay) b `shouldSatisfy` isJust
it "doubles the wait later each failure, up to a limit" $ do
let waits = map (retryAt 1) (drop 1 (iterate (cycleFailed 1) (book [1])))
cycleFailed a b = finishAddress t0 Failed a (snd (taken (later 1000000) b))
take 4 waits `shouldBe` map (Just . later) [2, 4, 8, 16]
waits !! 20 `shouldBe` Just (later maxBackoff)
it "forgets failures once the peer comes online" $ do
let failed = cycleWith Failed 1 (book [1])
(_, b) = taken (later 1000) failed
b' = finishAddress t0 Failed 1 (markConnected 1 b)
retryAt 1 b' `shouldBe` Just (later 2)
it "bans a discovered address that misbehaved" $
retryAt 1 (cycleWith Misbehaved 1 (book [1])) `shouldBe` Just (later banTime)
it "never bans a configured address" $ do
let b = addAddress Configured 1 (book [1])
retryAt 1 (cycleWith Misbehaved 1 b) `shouldBe` Just (later maxBackoff)
it "keeps a bounded number of discovered addresses" $ do
let b = book [1 .. maxDiscovered + 10]
b' = addAddress Configured (maxDiscovered + 20) b
length (knownAddresses b) `shouldBe` 4096
length (knownAddresses b') `shouldBe` 4096 + 1
it "keeps a bounded number of seed addresses" $ do
let b = foldl' (flip (addAddress Seeded)) emptyBook [1 .. 4106 :: Int]
length (knownAddresses b) `shouldBe` 4096
it "learns a new seed address when gossip has filled the book" $ do
let b = addAddress Seeded 99999 (book [1 .. 4096])
addressState 99999 b `shouldBe` Just Ready
it "replaces a gossiped address that failed when the book is full" $ do
let b = addAddress Discovered 5000 (cycleWith Failed 7 (book [1 .. 4096]))
addressState 5000 b `shouldBe` Just Ready
addressState 7 b `shouldBe` Nothing
length (knownAddresses b) `shouldBe` 4096
it "keeps gossiped addresses that have not failed when the book is full" $ do
let b = addAddress Discovered 5000 (book [1 .. 4096])
addressState 5000 b `shouldBe` Nothing
length (knownAddresses b) `shouldBe` 4096
describe "choice" $ do
it "takes a configured address before any other" $ do
let b = addAddress Configured 9 (book [1 .. 5])
fmap fst (takeAddress t0 3 (const True) b) `shouldBe` Just 9
it "takes only addresses that the predicate allows" $
picks even (book [1 .. 10]) `shouldBe` [2, 4, 6, 8, 10]
it "takes nothing when the predicate allows no address" $
takeAddress t0 0 (const False) (book [1 .. 10]) `shouldSatisfy` isNothing
it "can take any address, whatever its port" $ do
-- 20 addresses on port 8333 and 8 on one host with lower ports. Taking
-- them in the order of the map would always start with the low ports.
let v4 a p = SockAddrInet p (tupleToHostAddress a)
honest = [v4 (8, 8, 8, fromIntegral i) 8333 | i <- [1 .. 20 :: Int]]
attacker = [v4 (45, 1, 2, 3) p | p <- [1000 .. 1007]]
b = foldl' (flip (addAddress Discovered)) emptyBook (honest <> attacker)
picks (const True) b `shouldBe` sort (honest <> attacker)
describe "loading" $ do
it "loads when the book is empty" $
needsReload t0 (emptyBook :: AddressBook Int) `shouldBe` True
it "does not load while an address can be tried" $
needsReload t0 (book [1]) `shouldBe` False
it "does not load again too soon" $ do
-- banned, so that nothing can be tried for a long time
let b = markLoaded t0 (cycleWith Misbehaved 1 (book [1]))
needsReload (later 1) b `shouldBe` False
needsReload (later reloadInterval) b `shouldBe` True
prop "only hands out addresses that can be tried" $ \(ops :: [Op]) (d :: Int) ->
let b = foldl' apply emptyBook ops
now = later (fromIntegral (abs d))
in case takeAny now b of
Nothing -> property True
Just (a, b') ->
let ok = case addressState a b of
Just Ready -> True
Just (Waiting t _) -> t <= now
_ -> False
in ok .&&. addressState a b' === Just InUse
-- Four addresses never fill the book, so none has to make room.
prop "never forgets an address while the book has room" $ \(ops :: [Op]) ->
let b = foldl' apply emptyBook ops
added = [a | Add _ a <- ops]
in all (\a -> isJust (addressState a b)) added
prop "never keeps more than 4096 gossiped or 4096 seed addresses" $
forAll (listOf manyAddresses) $ \ops ->
let b = foldl' apply (book [1 .. 4096]) ops
from src = [a | a <- knownAddresses b, addressSource a b == Just src]
in length (from Discovered) <= 4096 .&&. length (from Seeded) <= 4096
where
-- Operations on 6000 addresses, on top of a book that is full already.
manyAddresses =
oneof
[ Add <$> elements [Seeded, Discovered] <*> chooseInt (1, 6000),
Take <$> (fromIntegral <$> chooseInt (0, 100000)) <*> arbitrary,
Finish
<$> (fromIntegral <$> chooseInt (0, 100000))
<*> elements [Disconnected, Failed, Misbehaved]
<*> chooseInt (1, 6000)
]