packages feed

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

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

module Moonlight.Delta.Patch.Internal.MultisetDeltaHash
  ( MultisetDeltaHash,
    buildMultisetDeltaHash,
    multisetDeltaHashState,
    multisetDeltaHashDigest,
    applyMultisetDeltaHash,
    composeMultisetDeltaHash,
  )
where

import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Vector.Unboxed qualified as UVector
import Data.Word (Word64)
import Moonlight.Algebra.Pure.LaneVector
  ( LaneVector,
    laneCount,
    laneVectorFromLanes,
    laneVectorLanes,
    laneVectorZero,
  )
import Moonlight.Core
  ( AdditiveGroup (sub),
    AdditiveMonoid (add),
    SipKey (..),
    StableHashDigest (..),
    StableHashEncoding,
    stableHashEncodingDigest,
    stableHashEncodingLength,
    stableHashEncodingWord8,
    stableHashEncodingWord64LE,
  )
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 (..),
    DeltaHashComposeError,
    DeltaHashEncoderVersion,
    DigestStrategy (..),
    MultisetDeltaHashDigest,
    IncrementalDigest (..),
    composeDeltaHashPatches,
    deltaHashEncodingVersion,
    rawDigestEncoding,
    sealMultisetDigest,
  )
import Moonlight.Delta.Patch.Internal.Types (Patch, PatchKey, PatchValue)
import Prelude

data MultisetDeltaHash key value = MultisetDeltaHash
  { multisetDeltaHashEncoderVersion :: !DeltaHashEncoderVersion,
    multisetDeltaHashKeyEncoding :: !(key -> StableHashEncoding),
    multisetDeltaHashValueEncoding :: !(value -> StableHashEncoding),
    multisetDeltaHashAuthoritativeState :: !(Map key value),
    multisetDeltaHashAccumulator :: !LaneVector
  }

buildMultisetDeltaHash ::
  DeltaHashEncoderVersion ->
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  Map key value ->
  MultisetDeltaHash key value
buildMultisetDeltaHash encoderVersion encodeKey encodeValue authoritativeState =
  MultisetDeltaHash
    { multisetDeltaHashEncoderVersion = encoderVersion,
      multisetDeltaHashKeyEncoding = encodeKey,
      multisetDeltaHashValueEncoding = encodeValue,
      multisetDeltaHashAuthoritativeState = authoritativeState,
      multisetDeltaHashAccumulator =
        Map.foldlWithKey'
          (addEncodedEntry encodeKey encodeValue)
          laneVectorZero
          authoritativeState
    }

multisetDeltaHashState :: MultisetDeltaHash key value -> Map key value
multisetDeltaHashState =
  multisetDeltaHashAuthoritativeState

-- | The homomorphic multiset commitment to the caller-encoded __current map
-- state__, compressed from a 16-lane accumulator in @(Z / 2^64 Z)^16@. Like the
-- Merkle digest it is a state commitment, not a history commitment, and not a
-- provenance record or MAC (the SipHash keys are fixed and public). Because the
-- accumulator is exactly additive, a non-injective value encoder collapses
-- distinct logical states to one digest: a replacement that subtracts and
-- re-adds an identical lane vector leaves the digest unchanged. Multiplicity is
-- modulo @2^64@ in every lane, so @2^64@ distinct keys with identical encodings
-- also contribute zero. The 1024-bit accumulator is compressed to 128 bits by
-- two fixed-key SipHash evaluations. Under the unproved ideal-independent-lane
-- assumption, generic collision work is about @2^64@ and preimage or second-
-- preimage work about @2^128@. The caller-supplied encoder version identifies
-- the encoding protocol; it does not prove encoder injectivity. The result is
-- strategy-indexed, so it cannot be compared with a 'MerkleDeltaHashDigest'.
multisetDeltaHashDigest :: MultisetDeltaHash key value -> MultisetDeltaHashDigest
multisetDeltaHashDigest deltaHashValue =
  digestLaneVector
    (multisetDeltaHashEncoderVersion deltaHashValue)
    (multisetDeltaHashAccumulator deltaHashValue)

applyMultisetDeltaHash ::
  (Ord key, Eq value) =>
  Patch key value ->
  MultisetDeltaHash key value ->
  Either (DeltaHashApplyError key value) (MultisetDeltaHash key value)
applyMultisetDeltaHash patchValue currentMultisetDeltaHash = do
  updatedState <-
    either
      (Left . DeltaHashPatchRejected)
      Right
      (Patch.apply patchValue (multisetDeltaHashAuthoritativeState currentMultisetDeltaHash))
  let !encodeKey = multisetDeltaHashKeyEncoding currentMultisetDeltaHash
      !encodeValue = multisetDeltaHashValueEncoding currentMultisetDeltaHash
      !authoritativeState = multisetDeltaHashAuthoritativeState currentMultisetDeltaHash
      !updatedAccumulator =
        Patch.foldWithKey'
          const
          (addEncodedEntry encodeKey encodeValue)
          (\accumulator key before ->
             let (storedKey, storedValue) =
                   storedEntryRepresentative authoritativeState key before
              in subtractEncodedEntry encodeKey encodeValue accumulator storedKey storedValue
          )
          (\accumulator key before after ->
             let (storedKey, storedValue) =
                   storedEntryRepresentative authoritativeState key before
              in addEncodedEntry
                   encodeKey
                   encodeValue
                   (subtractEncodedEntry encodeKey encodeValue accumulator storedKey storedValue)
                   key
                   after
          )
          (multisetDeltaHashAccumulator currentMultisetDeltaHash)
          patchValue
  pure
    MultisetDeltaHash
      { multisetDeltaHashEncoderVersion = multisetDeltaHashEncoderVersion currentMultisetDeltaHash,
        multisetDeltaHashKeyEncoding = encodeKey,
        multisetDeltaHashValueEncoding = encodeValue,
        multisetDeltaHashAuthoritativeState = updatedState,
        multisetDeltaHashAccumulator = updatedAccumulator
      }

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

addEncodedEntry ::
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  LaneVector ->
  key ->
  value ->
  LaneVector
addEncodedEntry encodeKey encodeValue accumulator key value =
  add accumulator (expand (encodeKey key) (encodeValue value))

subtractEncodedEntry ::
  (key -> StableHashEncoding) ->
  (value -> StableHashEncoding) ->
  LaneVector ->
  key ->
  value ->
  LaneVector
subtractEncodedEntry encodeKey encodeValue accumulator key value =
  sub accumulator (expand (encodeKey key) (encodeValue value))

expand :: StableHashEncoding -> StableHashEncoding -> LaneVector
expand keyEncoding valueEncoding =
  laneVectorFromLanes
    ( UVector.generate
        laneCount
        (\laneIndex ->
           stableHashDigestWord
             multisetExpansionSipKey
             (multisetElementEncoding laneIndex keyEncoding valueEncoding)
        )
    )

multisetElementEncoding ::
  Int ->
  StableHashEncoding ->
  StableHashEncoding ->
  StableHashEncoding
multisetElementEncoding laneIndex keyEncoding valueEncoding =
  stableHashEncodingWord8 3
    <> stableHashEncodingWord64LE deltaHashEncodingVersion
    <> stableHashEncodingWord64LE (fromIntegral (stableHashEncodingLength keyEncoding))
    <> keyEncoding
    <> stableHashEncodingWord64LE (fromIntegral (stableHashEncodingLength valueEncoding))
    <> valueEncoding
    <> stableHashEncodingWord64LE (fromIntegral laneIndex)

digestLaneVector :: DeltaHashEncoderVersion -> LaneVector -> MultisetDeltaHashDigest
digestLaneVector encoderVersion laneVector =
  sealMultisetDigest encoderVersion
    ( rawDigestEncoding
        multisetDigestSipKeyLane0
        multisetDigestSipKeyLane1
        ( stableHashEncodingWord8 4
            <> stableHashEncodingWord64LE deltaHashEncodingVersion
            <> stableHashEncodingWord64LE (fromIntegral laneCount)
            <> UVector.foldl'
              (\encoding lane -> encoding <> stableHashEncodingWord64LE lane)
              mempty
              (laneVectorLanes laneVector)
        )
    )

storedEntryRepresentative :: Ord key => Map key value -> key -> value -> (key, value)
storedEntryRepresentative authoritativeState patchKey patchValue =
  case Map.lookupGE patchKey authoritativeState of
    Just storedEntry@(storedKey, _storedValue)
      | compare storedKey patchKey == EQ -> storedEntry
    _ -> (patchKey, patchValue)

stableHashDigestWord :: SipKey -> StableHashEncoding -> Word64
stableHashDigestWord sipKey encoding =
  case stableHashEncodingDigest sipKey encoding of
    StableHashDigest digestWord -> digestWord

multisetExpansionSipKey :: SipKey
multisetExpansionSipKey =
  SipKey 0x6d6c2d64656c7461 0x6d756c74692d7632

multisetDigestSipKeyLane0 :: SipKey
multisetDigestSipKeyLane0 =
  SipKey 0x6d6c2d64656c7461 0x6d7365742d763031

multisetDigestSipKeyLane1 :: SipKey
multisetDigestSipKeyLane1 =
  SipKey 0x6d6c2d64656c7461 0x6d7365742d763032

instance IncrementalDigest MultisetDeltaHash where
  type IncrementalDigestError MultisetDeltaHash key value = DeltaHashApplyError key value
  type IncrementalDigestStrategy MultisetDeltaHash = 'MultisetDigestStrategy
  empty encoderVersion encodeKey encodeValue = buildMultisetDeltaHash encoderVersion encodeKey encodeValue Map.empty
  applyPatch = applyMultisetDeltaHash
  incrementalDigestState = multisetDeltaHashState
  digest = multisetDeltaHashDigest