packages feed

moonlight-delta-0.1.0.1: src-patch/Moonlight/Delta/Patch/Internal/MerkleDeltaHash.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}

module Moonlight.Delta.Patch.Internal.MerkleDeltaHash
  ( MerkleDeltaHash,
    buildMerkleDeltaHash,
    merkleDeltaHashState,
    merkleDeltaHashDigest,
    applyMerkleDeltaHash,
    composeMerkleDeltaHash,
    deltaHashFlatMaximumSize,
  )
where

import Data.Bits
  ( Bits (complement, shiftL, xor, (.&.), (.|.)),
    FiniteBits (countLeadingZeros, finiteBitSize),
  )
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Word (Word64)
import Moonlight.Core
  ( SipKey (..),
    StableHashDigest (..),
    StableHashEncoding,
    defaultStableHashKey,
  )
import Moonlight.Delta.Patch.Internal.Apply qualified as Patch
import Moonlight.Delta.Patch.Internal.Construction qualified as Patch
import Moonlight.Delta.Patch.Internal.IncrementalDigest
  ( DeltaHashApplyError (..),
    DeltaHashBuildError (..),
    DeltaHashComposeError,
    DeltaHashEncoderVersion,
    DigestStrategy (..),
    MerkleDeltaHashDigest,
    RawDigest128,
    IncrementalDigest (..),
    composeDeltaHashPatches,
    rawDigestEncodingChunks,
    sealMerkleDigest,
  )
import Moonlight.Delta.Patch.Internal.NodeCommitment qualified as Node
import Moonlight.Delta.Patch.Internal.Types (Patch, PatchKey, PatchValue)
import Prelude

data MerkleDeltaHash key value = MerkleDeltaHash
  { merkleDeltaHashEncoderVersion :: !DeltaHashEncoderVersion,
    merkleDeltaHashKeyEncoding :: !(key -> StableHashEncoding),
    merkleDeltaHashValueEncoding :: !(value -> StableHashEncoding),
    merkleDeltaHashAuthoritativeState :: !(Map key value),
    merkleDeltaHashDerivedView :: !(MerkleDeltaHashView key)
  }

data MerkleDeltaHashView key
  = FlatStableHash !RawDigest128
  | IncrementalMerkleHash !(MerkleTrie key)

data MerkleTrie key
  = MerkleEmpty
  | MerkleLeaf {-# UNPACK #-} !Word64 !key !RawDigest128
  | MerkleBranch
      {-# UNPACK #-} !Word64
      {-# UNPACK #-} !Word64
      !(MerkleTrie key)
      !(MerkleTrie key)
      !RawDigest128

buildMerkleDeltaHash ::
  Eq key =>
  DeltaHashEncoderVersion ->
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  Either (DeltaHashBuildError key) (MerkleDeltaHash key value)
buildMerkleDeltaHash encoderVersion encodeKey encodeValue authoritativeState =
  fmap
    (MerkleDeltaHash encoderVersion encodeKey encodeValue authoritativeState)
    (buildMerkleDeltaHashView encodeKey encodeValue authoritativeState)

merkleDeltaHashState :: MerkleDeltaHash key value -> Map key value
merkleDeltaHashState =
  merkleDeltaHashAuthoritativeState

-- | The Merkle commitment to the caller-encoded __current map state__. It is a
-- state commitment, not a history commitment: two different edit histories that
-- reach the same encoded map deliberately produce the same digest. It is not a
-- provenance record or a MAC — the SipHash keys are fixed and public — and its
-- collision resistance is only as strong as the caller's key and value
-- encoders. The result is strategy-indexed, so it cannot be compared with a
-- 'MultisetDeltaHashDigest'.
merkleDeltaHashDigest :: MerkleDeltaHash key value -> MerkleDeltaHashDigest
merkleDeltaHashDigest deltaHashValue =
  sealMerkleDigest
    (merkleDeltaHashEncoderVersion deltaHashValue)
    (merkleDeltaHashViewDigest (merkleDeltaHashDerivedView deltaHashValue))

applyMerkleDeltaHash ::
  (Ord key, Eq value) =>
  Patch key value ->
  MerkleDeltaHash key value ->
  Either (DeltaHashApplyError key value) (MerkleDeltaHash key value)
applyMerkleDeltaHash patchValue currentMerkleDeltaHash = do
  updatedState <-
    either
      (Left . DeltaHashPatchRejected)
      Right
      (Patch.apply patchValue (merkleDeltaHashAuthoritativeState currentMerkleDeltaHash))
  updatedView <-
    either
      (Left . DeltaHashUpdateRejected)
      Right
      ( updateMerkleDeltaHashView
          (merkleDeltaHashKeyEncoding currentMerkleDeltaHash)
          (merkleDeltaHashValueEncoding currentMerkleDeltaHash)
          (merkleDeltaHashAuthoritativeState currentMerkleDeltaHash)
          patchValue
          updatedState
          (merkleDeltaHashDerivedView currentMerkleDeltaHash)
      )
  pure
    MerkleDeltaHash
      { merkleDeltaHashEncoderVersion = merkleDeltaHashEncoderVersion currentMerkleDeltaHash,
        merkleDeltaHashKeyEncoding = merkleDeltaHashKeyEncoding currentMerkleDeltaHash,
        merkleDeltaHashValueEncoding = merkleDeltaHashValueEncoding currentMerkleDeltaHash,
        merkleDeltaHashAuthoritativeState = updatedState,
        merkleDeltaHashDerivedView = updatedView
      }

-- | Contextual composition costs @O(|state|)@ regardless of patch size; fold
-- many small patches first and apply once rather than composing pairwise.
composeMerkleDeltaHash ::
  (PatchKey key, PatchValue value) =>
  Patch key value ->
  Patch key value ->
  MerkleDeltaHash key value ->
  Either (DeltaHashComposeError key value) (Patch key value)
composeMerkleDeltaHash =
  composeDeltaHashPatches

buildMerkleDeltaHashView ::
  Eq key =>
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  Either (DeltaHashBuildError key) (MerkleDeltaHashView key)
buildMerkleDeltaHashView encodeKey encodeValue authoritativeState
  | usesFlatStableHash authoritativeState =
      Right (FlatStableHash (flatStateDigest encodeKey encodeValue authoritativeState))
  | otherwise =
      fmap IncrementalMerkleHash (buildMerkleTrie encodeKey encodeValue authoritativeState)

updateMerkleDeltaHashView ::
  Ord key =>
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  Patch key value ->
  Map key value ->
  MerkleDeltaHashView key ->
  Either (DeltaHashBuildError key) (MerkleDeltaHashView key)
updateMerkleDeltaHashView encodeKey encodeValue authoritativeState patchValue updatedState currentView
  | usesFlatStableHash updatedState =
      Right (FlatStableHash (flatStateDigest encodeKey encodeValue updatedState))
  | otherwise =
      case currentView of
        FlatStableHash _digest ->
          fmap IncrementalMerkleHash (buildMerkleTrie encodeKey encodeValue updatedState)
        IncrementalMerkleHash trie ->
          fmap IncrementalMerkleHash (applyPatchToTrie encodeKey encodeValue authoritativeState patchValue trie)

merkleDeltaHashViewDigest :: MerkleDeltaHashView key -> RawDigest128
merkleDeltaHashViewDigest view =
  case view of
    FlatStableHash digestValue -> digestValue
    IncrementalMerkleHash trie -> merkleTrieDigest trie

usesFlatStableHash :: Map key value -> Bool
usesFlatStableHash authoritativeState =
  Map.size authoritativeState <= deltaHashFlatMaximumSize

flatStateDigest ::
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  RawDigest128
flatStateDigest encodeKey encodeValue authoritativeState =
  rawDigestEncodingChunks
    defaultStableHashKey
    flatSipKeyLane1
    (either encodeKey encodeValue)
    (flatStateEncodings authoritativeState)

flatStateEncodings :: Map key value -> [Either key value]
flatStateEncodings =
  Map.foldrWithKey (\key value encodings -> Left key : Right value : encodings) []

buildMerkleTrie ::
  Eq key =>
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  Either (DeltaHashBuildError key) (MerkleTrie key)
buildMerkleTrie encodeKey encodeValue =
  Map.foldlWithKey' insertEntry (Right MerkleEmpty)
  where
    insertEntry accumulated key value =
      accumulated >>= insertEncodedValue encodeKey encodeValue key value

applyPatchToTrie ::
  Ord key =>
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  Patch key value ->
  MerkleTrie key ->
  Either (DeltaHashBuildError key) (MerkleTrie key)
applyPatchToTrie encodeKey encodeValue authoritativeState patchValue initialTrie =
  Patch.foldWithKey'
    const
    (\accumulated key after -> accumulated >>= insertEncodedValue encodeKey encodeValue key after)
    (\accumulated _key _before -> accumulated)
    (\accumulated key _before after -> accumulated >>= insertEncodedValue encodeKey encodeValue key after)
    (Right (retirePatchPaths encodeKey authoritativeState patchValue initialTrie))
    patchValue

insertEncodedValue ::
  Eq key =>
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  key ->
  value ->
  MerkleTrie key ->
  Either (DeltaHashBuildError key) (MerkleTrie key)
insertEncodedValue encodeKey encodeValue key value =
  let !keyEncoding = encodeKey key
      !path = Node.localizePath Node.sipHashNodeCommitment keyEncoding
      !leafCommitment =
        Node.leafDigest Node.sipHashNodeCommitment path keyEncoding (encodeValue value)
   in insertMerkleLeaf path key leafCommitment

deleteEncodedKey ::
  Eq key =>
  (key -> StableHashEncoding) ->
  key ->
  MerkleTrie key ->
  MerkleTrie key
deleteEncodedKey encodeKey key =
  deleteMerkleLeaf (stableHashPathWord (encodeKey key)) key

retirePatchPaths ::
  Ord key =>
  (key -> StableHashEncoding) ->
  Map key value ->
  Patch key value ->
  MerkleTrie key ->
  MerkleTrie key
retirePatchPaths encodeKey authoritativeState patchValue initialTrie =
  Patch.foldWithKey'
    const
    (\trie _key _after -> trie)
    (\trie key _before -> deleteAuthoritativeKey encodeKey authoritativeState key trie)
    (\trie key _before _after -> deleteMovedAuthoritativeKey encodeKey authoritativeState key trie)
    initialTrie
    patchValue

deleteAuthoritativeKey ::
  Ord key =>
  (key -> StableHashEncoding) ->
  Map key value ->
  key ->
  MerkleTrie key ->
  MerkleTrie key
deleteAuthoritativeKey encodeKey authoritativeState patchKey trie =
  case Map.lookupGE patchKey authoritativeState of
    Just (storedKey, _value)
      | compare storedKey patchKey == EQ -> deleteEncodedKey encodeKey storedKey trie
    _ -> trie

deleteMovedAuthoritativeKey ::
  Ord key =>
  (key -> StableHashEncoding) ->
  Map key value ->
  key ->
  MerkleTrie key ->
  MerkleTrie key
deleteMovedAuthoritativeKey encodeKey authoritativeState patchKey trie =
  case Map.lookupGE patchKey authoritativeState of
    Just (storedKey, _value)
      | compare storedKey patchKey == EQ ->
          let !storedPath = stableHashPathWord (encodeKey storedKey)
              !patchPath = stableHashPathWord (encodeKey patchKey)
           in if storedPath == patchPath
                then trie
                else deleteMerkleLeaf storedPath storedKey trie
    _ -> trie

insertMerkleLeaf ::
  Eq key =>
  Word64 ->
  key ->
  RawDigest128 ->
  MerkleTrie key ->
  Either (DeltaHashBuildError key) (MerkleTrie key)
insertMerkleLeaf path key leafCommitment trie =
  case trie of
    MerkleEmpty ->
      Right (merkleLeaf path key leafCommitment)
    MerkleLeaf existingPath existingKey _existingDigest
      | path == existingPath ->
          if key == existingKey
            then Right (merkleLeaf path key leafCommitment)
            else
              Left
                DeltaHashKeyCollision
                  { deltaHashCollisionDigest = StableHashDigest path,
                    deltaHashExistingKey = existingKey,
                    deltaHashIncomingKey = key
                  }
      | otherwise ->
          Right (linkMerkleTries path (merkleLeaf path key leafCommitment) existingPath trie)
    branch@(MerkleBranch prefix branchingBit left right _branchDigest)
      | not (matchesPrefix path prefix branchingBit) ->
          Right (linkMerkleTries path (merkleLeaf path key leafCommitment) prefix branch)
      | zeroAtBit path branchingBit ->
          fmap
            (\updatedLeft -> merkleBranch prefix branchingBit updatedLeft right)
            (insertMerkleLeaf path key leafCommitment left)
      | otherwise ->
          fmap
            (\updatedRight -> merkleBranch prefix branchingBit left updatedRight)
            (insertMerkleLeaf path key leafCommitment right)

deleteMerkleLeaf ::
  Eq key =>
  Word64 ->
  key ->
  MerkleTrie key ->
  MerkleTrie key
deleteMerkleLeaf path key trie =
  case trie of
    MerkleEmpty -> MerkleEmpty
    MerkleLeaf existingPath existingKey _digest ->
      if path == existingPath && key == existingKey then MerkleEmpty else trie
    MerkleBranch prefix branchingBit left right _branchDigest
      | not (matchesPrefix path prefix branchingBit) -> trie
      | zeroAtBit path branchingBit ->
          compactMerkleBranch prefix branchingBit (deleteMerkleLeaf path key left) right
      | otherwise ->
          compactMerkleBranch prefix branchingBit left (deleteMerkleLeaf path key right)

linkMerkleTries ::
  Word64 ->
  MerkleTrie key ->
  Word64 ->
  MerkleTrie key ->
  MerkleTrie key
linkMerkleTries leftPath leftTrie rightPath rightTrie =
  let !branchingBit = highestDifferingBit leftPath rightPath
      !prefix = maskPrefix leftPath branchingBit
   in if zeroAtBit leftPath branchingBit
        then merkleBranch prefix branchingBit leftTrie rightTrie
        else merkleBranch prefix branchingBit rightTrie leftTrie

compactMerkleBranch ::
  Word64 ->
  Word64 ->
  MerkleTrie key ->
  MerkleTrie key ->
  MerkleTrie key
compactMerkleBranch prefix branchingBit left right =
  case (left, right) of
    (MerkleEmpty, remaining) -> remaining
    (remaining, MerkleEmpty) -> remaining
    _ -> merkleBranch prefix branchingBit left right

merkleLeaf :: Word64 -> key -> RawDigest128 -> MerkleTrie key
merkleLeaf path key leafCommitment =
  MerkleLeaf path key leafCommitment

merkleBranch ::
  Word64 ->
  Word64 ->
  MerkleTrie key ->
  MerkleTrie key ->
  MerkleTrie key
merkleBranch prefix branchingBit left right =
  MerkleBranch
    prefix
    branchingBit
    left
    right
    (Node.branchDigest Node.sipHashNodeCommitment prefix branchingBit (merkleTrieDigest left) (merkleTrieDigest right))

merkleTrieDigest :: MerkleTrie key -> RawDigest128
merkleTrieDigest trie =
  case trie of
    MerkleEmpty -> Node.emptyDigest Node.sipHashNodeCommitment
    MerkleLeaf _path _key digestValue -> digestValue
    MerkleBranch _prefix _branchingBit _left _right digestValue -> digestValue

stableHashPathWord :: StableHashEncoding -> Word64
stableHashPathWord =
  Node.localizePath Node.sipHashNodeCommitment


deltaHashFlatMaximumSize :: Int
deltaHashFlatMaximumSize =
  256

flatSipKeyLane1 :: SipKey
flatSipKeyLane1 =
  SipKey 0x6d6f6f6e6c696768 0x742d636f72652d32

matchesPrefix :: Word64 -> Word64 -> Word64 -> Bool
matchesPrefix path prefix branchingBit =
  maskPrefix path branchingBit == prefix

maskPrefix :: Word64 -> Word64 -> Word64
maskPrefix path branchingBit =
  path .&. complement (branchingBit .|. (branchingBit - 1))

zeroAtBit :: Word64 -> Word64 -> Bool
zeroAtBit path branchingBit =
  path .&. branchingBit == 0

highestDifferingBit :: Word64 -> Word64 -> Word64
highestDifferingBit leftPath rightPath =
  let !differentBits = leftPath `xor` rightPath
      !highestIndex = finiteBitSize differentBits - countLeadingZeros differentBits - 1
   in 1 `shiftL` highestIndex

emptyMerkleDeltaHash ::
  DeltaHashEncoderVersion ->
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  MerkleDeltaHash key value
emptyMerkleDeltaHash encoderVersion encodeKey encodeValue =
  MerkleDeltaHash encoderVersion encodeKey encodeValue Map.empty (FlatStableHash (flatStateDigest encodeKey encodeValue Map.empty))

instance IncrementalDigest MerkleDeltaHash where
  type IncrementalDigestError MerkleDeltaHash key value = DeltaHashApplyError key value
  type IncrementalDigestStrategy MerkleDeltaHash = 'MerkleDigestStrategy
  empty = emptyMerkleDeltaHash
  applyPatch = applyMerkleDeltaHash
  incrementalDigestState = merkleDeltaHashState
  digest = merkleDeltaHashDigest