Skip to content

Commit

Permalink
Try #3068:
Browse files Browse the repository at this point in the history
  • Loading branch information
iohk-bors[bot] committed Jan 5, 2022
2 parents 2cb11cc + 7e5955a commit d207a31
Show file tree
Hide file tree
Showing 13 changed files with 721 additions and 473 deletions.
2 changes: 2 additions & 0 deletions lib/core/cardano-wallet-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ library
Cardano.Pool.DB.Sqlite.TH
Cardano.Pool.Metadata
Cardano.Wallet
Cardano.Wallet.Address.Pool
Cardano.Wallet.Api
Cardano.Wallet.Api.Client
Cardano.Wallet.Api.Link
Expand Down Expand Up @@ -408,6 +409,7 @@ test-suite unit
Cardano.Pool.DB.MVarSpec
Cardano.Pool.DB.Properties
Cardano.Pool.DB.SqliteSpec
Cardano.Wallet.Address.PoolSpec
Cardano.Wallet.Api.Malformed
Cardano.Wallet.Api.Server.TlsSpec
Cardano.Wallet.Api.ServerSpec
Expand Down
35 changes: 10 additions & 25 deletions lib/core/src/Cardano/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -286,20 +286,13 @@ import Cardano.Wallet.Primitive.AddressDiscovery
import Cardano.Wallet.Primitive.AddressDiscovery.Random
( ErrImportAddress (..), RndStateLike )
import Cardano.Wallet.Primitive.AddressDiscovery.Sequential
( ParentContext (..)
, SeqState
, defaultAddressPoolGap
, mkSeqStateFromRootXPrv
, purposeBIP44
)
( SeqState, defaultAddressPoolGap, mkSeqStateFromRootXPrv, purposeBIP44 )
import Cardano.Wallet.Primitive.AddressDiscovery.Shared
( CredentialType (..)
, ErrAddCosigner (..)
, ErrScriptTemplate (..)
, SharedState (..)
, SharedStateFields (..)
, addCosignerAccXPub
, isShared
)
import Cardano.Wallet.Primitive.CoinSelection
( Selection
Expand Down Expand Up @@ -744,7 +737,6 @@ createIcarusWallet
, PaymentAddress n k
, k ~ IcarusKey
, s ~ SeqState n k
, Typeable n
)
=> ctx
-> WalletId
Expand Down Expand Up @@ -2907,11 +2899,11 @@ updateCosigner
-> Cosigner
-> CredentialType
-> ExceptT ErrAddCosignerKey IO ()
updateCosigner ctx wid accXPub cosigner cred = db & \DBLayer{..} -> do
updateCosigner ctx wid cosignerXPub cosigner cred = db & \DBLayer{..} -> do
mapExceptT atomically $ do
cp <- withExceptT ErrAddCosignerKeyNoSuchWallet $ withNoSuchWallet wid $
readCheckpoint wid
case addCosignerAccXPub accXPub cosigner cred (getState cp) of
case addCosignerAccXPub (cosigner, cosignerXPub) cred (getState cp) of
Left err -> throwE (ErrAddCosignerKey err)
Right st' -> withExceptT ErrAddCosignerKeyNoSuchWallet $
putCheckpoint wid (updateState st' cp)
Expand All @@ -2923,23 +2915,16 @@ updateCosigner ctx wid accXPub cosigner cred = db & \DBLayer{..} -> do
-- base addresses (containing both payment and delegation credentials).
-- So we normalize them all to be base addresses to make sure that we compare them correctly.
normalizeSharedAddress
:: forall s k n.
( MkKeyFingerprint k Address
, MkKeyFingerprint k (Proxy n, k 'AddressK XPub)
, s ~ SharedState n k
, k ~ SharedKey
, SoftDerivation k
, Typeable n
)
=> s
:: forall n k. ( Shared.SupportsDiscovery n k, k ~ SharedKey )
=> SharedState n k
-> Address
-> Maybe Address
normalizeSharedAddress s@(SharedState _ state') addr = case state' of
PendingFields _ -> Nothing
ReadyFields pool -> do
let (ParentContextShared _ _ dTM) = Seq.context pool
normalizeSharedAddress st addr = case Shared.ready st of
Shared.Pending -> Nothing
Shared.Active _ -> do
let dTM = Shared.delegationTemplate st
fingerprint <- eitherToMaybe (paymentKeyFingerprint @k addr)
let (ixM, _) = isShared addr s
let (ixM, _) = Shared.isShared addr st
case (dTM, ixM) of
(Just dT, Just ix) ->
pure $ Shared.liftDelegationAddress @n ix dT fingerprint
Expand Down
228 changes: 228 additions & 0 deletions lib/core/src/Cardano/Wallet/Address/Pool.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

-- |
-- Copyright: © 2021 IOHK
-- License: Apache-2.0
--
-- An address pool caches a collection of addresses.
-- The purpose of this data structure is to aid in BIP-44 style
-- address discovery with an address gap.
module Cardano.Wallet.Address.Pool
( Pool
, generator
, addresses
, gap
, lookup
, size
, next
, new
, load
, update
, clear

-- * Internal
, loadUnsafe
, prop_sequence
, prop_gap
, prop_fresh
, prop_generator
, prop_consistent
)
where

import Prelude hiding
( last, lookup )

import Cardano.Wallet.Primitive.Types.Address
( AddressState (..) )
import Control.DeepSeq
( NFData )
import Data.Map.Strict
( Map )
import Data.Ord
( Down (..) )
import Fmt
( Buildable (..) )
import GHC.Generics
( Generic )

{- HLINT ignore "Avoid restricted qualification" -}
import qualified Data.List as List
import qualified Data.Map.Strict as Map

{-------------------------------------------------------------------------------
Address Pool, abstract data type
-------------------------------------------------------------------------------}
-- | An address pool caches a collection of addresses (type @addr@)
-- which are derived from a numeric index (type @ix@).
data Pool addr ix = Pool
{ generator :: ix -> addr
-- ^ Mapping from a numeric index to its corresponding address.
--
-- This mapping is supposed to be (practically) a one-way function:
-- Given an 'addr', it is impossible to compute the preimage
-- 'ix' in practice.
-- The purpose of the 'Pool' data structure is to help inverting
-- this function regardless. The idea is that addresses
-- with small indices @0,1,…@ are 'Used' before addresses with larger
-- indices; specifically, only less than 'gap' many addresses in sequence
-- may be 'Unused' before the next 'Used' address.
-- This usage scheme restricts the search space considerably
-- and allows us to practically invert the 'generator' function.
, gap :: Int
-- ^ The pool gap determines how 'Used' and 'Unused'
-- have to be distributed.
-- See 'prop_gap' and 'prop_fresh'.
, addresses :: Map addr (ix, AddressState)
-- ^ Partial, cached inverse of the 'generator'.
-- This map contains all cached addresses @addr@,
-- their corresponding indices @ix@,
-- and whether they are 'Used' or 'Unused'.
-- See 'prop_sequence'.
} deriving (Generic)

instance (NFData addr, NFData ix) => NFData (Pool addr ix)

-- | Internal invariant:
-- The indices of the addresses in a pool form a finite
-- sequence beginning with 'fromEnum'@ 0@.
prop_sequence :: (Ord ix, Enum ix) => Pool addr ix -> Bool
prop_sequence Pool{addresses} =
indices `List.isPrefixOf` [toEnum 0..]
where
indices = List.sort $ map fst $ Map.elems addresses

-- | Internal invariant:
-- If we order the 'addresses' by their indices,
-- then there are always /less than/ 'gap' many 'Unused'
-- addresses between two consecutive 'Used' addresses,
-- or before the first 'Used' address.
prop_gap :: Ord ix => Pool addr ix -> Bool
prop_gap Pool{gap,addresses}
= all (< gap) . consecutiveUnused . List.group $ statuses
where
consecutiveUnused ((Used:_):xs) = consecutiveUnused xs
consecutiveUnused (x@(Unused:_):(Used:_):xs) =
length x : consecutiveUnused xs
consecutiveUnused _ = []

statuses = map snd $ List.sortOn fst $ Map.elems addresses

-- | Internal invariant:
-- If we order the 'addresses' by their indices,
-- there are exactly 'gap' many 'Unused' addresses after the last
-- 'Used' address.
prop_fresh :: Ord ix => Pool addr ix -> Bool
prop_fresh Pool{gap,addresses} =
takeWhile (== Unused) end == replicate gap Unused
where
end = map snd $ List.sortOn (Down . fst) $ Map.elems addresses

-- | Internal invariant:
-- All 'addresses' in the pool have been generated from their index
-- via the pool 'generator'.
prop_generator :: Eq addr => Pool addr ix -> Bool
prop_generator Pool{generator,addresses} =
and $ Map.mapWithKey isGenerated addresses
where
isGenerated addr (ix,_) = generator ix == addr

-- | Internal invariant: The pool satisfies all invariants above.
prop_consistent :: (Ord ix, Enum ix, Eq addr) => Pool addr ix -> Bool
prop_consistent p =
all ($ p) [prop_sequence, prop_gap, prop_fresh, prop_generator]

{-------------------------------------------------------------------------------
Pretty printing
-------------------------------------------------------------------------------}
instance Buildable (Pool addr ix) where
build pool = "AddressPool "
<> "{ " <> build (size pool) <> " addresses"
<> ", gap = " <> build (gap pool)
<> "}"

instance (Show addr, Show ix) => Show (Pool addr ix) where
show pool = "AddressPool"
<> "{ generator = <<function>>"
<> ", gap = " <> show (gap pool)
<> ", addresses = " <> show (addresses pool)
<> "}"

{-------------------------------------------------------------------------------
Address Pool, operations
-------------------------------------------------------------------------------}
-- | Create a new address pool.
new :: (Ord addr, Enum ix) => (ix -> addr) -> Int -> Pool addr ix
new generator gap
= ensureFresh (toEnum 0) $ Pool{ generator, gap, addresses = Map.empty }

-- | Replace the collection of addresses in a pool,
-- but only if this collection satisfies the necessary invariants
-- such as 'prop_sequence' etc.
load
:: (Ord addr, Ord ix, Enum ix)
=> Pool addr ix -> Map addr (ix,AddressState) -> Maybe (Pool addr ix)
load pool0 addrs = if prop_consistent pool then Just pool else Nothing
where pool = loadUnsafe pool0 addrs

-- | Replace the collection of addresses in a pool,
-- but skips checking the invariants.
loadUnsafe :: Pool addr ix -> Map addr (ix,AddressState) -> Pool addr ix
loadUnsafe pool addrs = pool{ addresses = addrs }

-- | Remove all previously discovered addresses,
-- i.e. create a new pool with the same 'generator' and 'gap' as the old pool.
clear :: (Ord addr, Enum ix) => Pool addr ix -> Pool addr ix
clear Pool{generator,gap} = new generator gap

-- | Look up an address in the pool.
lookup :: Ord addr => addr -> Pool addr ix -> Maybe ix
lookup addr Pool{addresses} = fst <$> Map.lookup addr addresses

-- | Number of addresses cached in the pool.
size :: Pool addr ix -> Int
size = Map.size . addresses

-- | Given an index, retrieve the next index that is still in the pool.
next :: Enum ix => Pool addr ix -> ix -> Maybe ix
next Pool{addresses} ix = let jx = succ ix in
if fromEnum jx >= Map.size addresses then Nothing else Just jx

-- | Update an address to the 'Used' status
-- and create new 'Unused' addresses in order to satisfy 'prop_fresh'.
--
-- Does nothing if the address was not in the pool.
update :: (Ord addr, Enum ix) => addr -> Pool addr ix -> Pool addr ix
update addr pool@Pool{addresses} =
case Map.lookup addr addresses of
Nothing -> pool
Just (ix,_) -> ensureFresh (succ ix) $ pool
{ addresses = Map.adjust (\(i,_) -> (i, Used)) addr addresses }

-- | Create additional 'Unused' addresses from larger indices
-- in order to satisfy 'prop_fresh' again.
--
-- Preconditions:
--
-- * The index @ix@ satisfies:
--
-- either @ix = fromEnum 0@
-- or @ix = succ jx@ and the index @jx@ is a 'Used' address.
--
-- * All addresses with index @ix@ or larger are 'Unused'.
ensureFresh :: (Ord addr, Enum ix) => ix -> Pool addr ix -> Pool addr ix
ensureFresh ix pool@Pool{generator,gap,addresses}
= pool { addresses = Map.union addresses nexts }
where
fresh = toEnum $ Map.size addresses -- first index that is not in the pool
nexts = Map.fromList
[ (generator i, (i, Unused)) | i <- [fresh .. to] ]
where
to = toEnum $ fromEnum ix + fromIntegral gap - 1
-- example:
-- ix = 0 && fresh = 0 && gap = 20 `implies` [fresh .. to] = [0..19]

0 comments on commit d207a31

Please sign in to comment.