-- | Record validation for the Kademlia DHT.
--
-- Per specs/kad-dht (Entry validation), records must be validated on
-- two occasions: values retrieved in a GET_VALUE query and values
-- received in a PUT_VALUE query before storing them locally.
--
-- Record keys have the form @/namespace/path@; validation dispatches on
-- the namespace, mirroring go-libp2p's @record.NamespacedValidator@.
-- The built-in @/pk/@ validator binds the key to the value: the path
-- must be the multihash (Peer ID) of the serialized public key carried
-- in the value.
module LibP2P.DHT.Validator
  ( -- * Validator interface
    Validator (..)
    -- * Built-in validators
  , namespacedValidator
  , pkValidator
  , defaultValidator
    -- * Key helpers
  , splitRecordKey
  ) where

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import LibP2P.Crypto.PeerId (fromPublicKey, peerIdBytes)
import LibP2P.Crypto.Protobuf (decodePublicKey)

-- | Validator interface for record validation.
--
-- 'valValidate' checks that a value is well-formed for its key and
-- returns an error for records that must not be stored or served.
-- 'valSelect' picks the index of the best value among conflicting
-- candidates for the same key (used for GET_VALUE conflict resolution).
data Validator = Validator
  { Validator -> ByteString -> ByteString -> Either String ()
valValidate :: ByteString -> ByteString -> Either String ()
  , Validator -> ByteString -> [ByteString] -> Either String Int
valSelect   :: ByteString -> [ByteString] -> Either String Int
  }

-- | Split a record key of the form @/namespace/path@ into
-- (namespace, path). The path is raw bytes (for @/pk/@ it is a binary
-- multihash), so only the two leading separators are interpreted.
splitRecordKey :: ByteString -> Either String (ByteString, ByteString)
splitRecordKey :: ByteString -> Either String (ByteString, ByteString)
splitRecordKey ByteString
key = case ByteString -> Maybe (Word8, ByteString)
BS.uncons ByteString
key of
  Just (Word8
0x2F, ByteString
rest) ->
    let (ByteString
ns, ByteString
pathWithSlash) = (Word8 -> Bool) -> ByteString -> (ByteString, ByteString)
BS.break (Word8 -> Word8 -> Bool
forall a. Eq a => a -> a -> Bool
== Word8
0x2F) ByteString
rest
    in case ByteString -> Maybe (Word8, ByteString)
BS.uncons ByteString
pathWithSlash of
         Just (Word8
0x2F, ByteString
path) -> (ByteString, ByteString) -> Either String (ByteString, ByteString)
forall a b. b -> Either a b
Right (ByteString
ns, ByteString
path)
         Maybe (Word8, ByteString)
_ -> String -> Either String (ByteString, ByteString)
forall a b. a -> Either a b
Left String
"invalid record key: missing namespace separator"
  Maybe (Word8, ByteString)
_ -> String -> Either String (ByteString, ByteString)
forall a b. a -> Either a b
Left String
"invalid record key: missing leading '/'"

-- | Dispatch validation by key namespace. Keys without a registered
-- namespace are rejected, matching go-libp2p's namespaced validator
-- (\"invalid record keytype\").
namespacedValidator :: Map ByteString Validator -> Validator
namespacedValidator :: Map ByteString Validator -> Validator
namespacedValidator Map ByteString Validator
validators = Validator
  { valValidate :: ByteString -> ByteString -> Either String ()
valValidate = \ByteString
key ByteString
value -> do
      v <- ByteString -> Either String Validator
validatorFor ByteString
key
      valValidate v key value
  , valSelect :: ByteString -> [ByteString] -> Either String Int
valSelect = \ByteString
key [ByteString]
values -> do
      v <- ByteString -> Either String Validator
validatorFor ByteString
key
      valSelect v key values
  }
  where
    validatorFor :: ByteString -> Either String Validator
validatorFor ByteString
key = do
      (ns, _) <- ByteString -> Either String (ByteString, ByteString)
splitRecordKey ByteString
key
      case Map.lookup ns validators of
        Maybe Validator
Nothing -> String -> Either String Validator
forall a b. a -> Either a b
Left (String
"invalid record keytype: " String -> String -> String
forall a. [a] -> [a] -> [a]
++ ByteString -> String
BSC.unpack ByteString
ns)
        Just Validator
v -> Validator -> Either String Validator
forall a b. b -> Either a b
Right Validator
v

-- | Validator for the @/pk/@ namespace: the value must be a serialized
-- PublicKey protobuf whose derived Peer ID equals the multihash in the
-- key path. Public keys never conflict, so 'valSelect' keeps the first
-- candidate (go-libp2p's @record.PublicKeyValidator@ does the same).
pkValidator :: Validator
pkValidator :: Validator
pkValidator = Validator
  { valValidate :: ByteString -> ByteString -> Either String ()
valValidate = \ByteString
key ByteString
value -> do
      (_, mh) <- ByteString -> Either String (ByteString, ByteString)
splitRecordKey ByteString
key
      pub <- decodePublicKey value
      let derived = PeerId -> ByteString
peerIdBytes (PublicKey -> PeerId
fromPublicKey PublicKey
pub)
      if derived == mh
        then Right ()
        else Left "public key does not match record key"
  , valSelect :: ByteString -> [ByteString] -> Either String Int
valSelect = \ByteString
_ [ByteString]
values ->
      if [ByteString] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [ByteString]
values
        then String -> Either String Int
forall a b. a -> Either a b
Left String
"no values to select from"
        else Int -> Either String Int
forall a b. b -> Either a b
Right Int
0
  }

-- | The default validator set: @/pk/@ records only. Additional
-- namespaces can be registered by building a custom
-- 'namespacedValidator' and storing it in the DHT node.
defaultValidator :: Validator
defaultValidator :: Validator
defaultValidator = Map ByteString Validator -> Validator
namespacedValidator ([(ByteString, Validator)] -> Map ByteString Validator
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [(String -> ByteString
BSC.pack String
"pk", Validator
pkValidator)])