packages feed

moonlight-delta-0.1.0.1: test/patch/DeltaHashGoldenOracle.hs

{-# LANGUAGE DerivingStrategies #-}

module DeltaHashGoldenOracle
  ( OracleDigest (..),
    oracleEmptyNodeDigest,
    oracleFlatDigest,
    oracleMultisetDigest,
    oracleSipHashReferenceVectors,
    oracleTrieDigest,
  )
where

import Control.Monad (foldM)
import Data.Bits
  ( Bits (complement, rotateL, shiftL, shiftR, xor, (.&.), (.|.)),
    FiniteBits (countLeadingZeros, finiteBitSize),
  )
import Data.List (foldl', unfoldr)
import Data.Word (Word8, Word64)
import Prelude

data OracleDigest = OracleDigest !Word64 !Word64
  deriving stock (Eq, Ord, Show)

data SipState = SipState !Word64 !Word64 !Word64 !Word64

data OracleTrie
  = OracleEmpty
  | OracleLeaf !Word64 !OracleDigest
  | OracleBranch !Word64 !Word64 !OracleTrie !OracleTrie !OracleDigest

oracleFlatDigest :: OracleDigest
oracleFlatDigest =
  digestWithKeys
    defaultLane0Key
    defaultLane1Key
    (word64LE deltaHashProtocolVersion <> compactWord64 4 <> foldMap frameChunk [word64LE 1, word64LE 10, word64LE 2, word64LE 20])

oracleEmptyNodeDigest :: OracleDigest
oracleEmptyNodeDigest =
  digestWithKeys
    emptyLane0Key
    emptyLane1Key
    (0 : word64LE deltaHashProtocolVersion)

oracleTrieDigest :: Maybe OracleDigest
oracleTrieDigest =
  fmap trieDigest
    (foldM insertEntry OracleEmpty [(key, key) | key <- [0 .. 256]])
  where
    insertEntry trie (key, value) =
      insertLeaf
        (sipHash pathKey (word64LE key))
        (leafDigest key value)
        trie

oracleMultisetDigest :: OracleDigest
oracleMultisetDigest =
  digestWithKeys
    multisetLane0Key
    multisetLane1Key
    ( 4
        : word64LE deltaHashProtocolVersion
          <> word64LE 16
          <> foldMap word64LE accumulatedLanes
    )
  where
    accumulatedLanes =
      foldl'
        (zipWith (+))
        (replicate 16 0)
        [expandedEntry 1 10, expandedEntry 2 20]

oracleSipHashReferenceVectors :: [Word64]
oracleSipHashReferenceVectors =
  fmap
    ( \messageLength ->
        sipHash
          (0x0706050403020100, 0x0f0e0d0c0b0a0908)
          (take messageLength [0 ..])
    )
    [0 .. 63]

expandedEntry :: Word64 -> Word64 -> [Word64]
expandedEntry key value =
  fmap
    ( \laneIndex ->
        sipHash
          multisetExpansionKey
          ( 3
              : word64LE deltaHashProtocolVersion
                <> word64LE 8
                <> word64LE key
                <> word64LE 8
                <> word64LE value
                <> word64LE laneIndex
          )
    )
    [0 .. 15]

leafDigest :: Word64 -> Word64 -> OracleDigest
leafDigest key value =
  digestWithKeys
    leafLane0Key
    leafLane1Key
    ( 1
        : word64LE deltaHashProtocolVersion
          <> word64LE path
          <> word64LE 8
          <> word64LE key
          <> word64LE 8
          <> word64LE value
    )
  where
    path = sipHash pathKey (word64LE key)

branchDigest :: Word64 -> Word64 -> OracleTrie -> OracleTrie -> OracleDigest
branchDigest prefix branchingBit left right =
  digestWithKeys
    branchLane0Key
    branchLane1Key
    ( 2
        : word64LE deltaHashProtocolVersion
          <> word64LE prefix
          <> word64LE branchingBit
          <> digestBytes (trieDigest left)
          <> digestBytes (trieDigest right)
    )

insertLeaf :: Word64 -> OracleDigest -> OracleTrie -> Maybe OracleTrie
insertLeaf path digestValue trie =
  case trie of
    OracleEmpty ->
      Just (OracleLeaf path digestValue)
    OracleLeaf existingPath _existingDigest
      | path == existingPath ->
          Nothing
      | otherwise ->
          linkTries path (OracleLeaf path digestValue) existingPath trie
    OracleBranch prefix branchingBit left right _branchDigest
      | maskPrefix path branchingBit /= prefix ->
          linkTries path (OracleLeaf path digestValue) prefix trie
      | zeroAtBit path branchingBit ->
          fmap (\updatedLeft -> oracleBranch prefix branchingBit updatedLeft right) (insertLeaf path digestValue left)
      | otherwise ->
          fmap (oracleBranch prefix branchingBit left) (insertLeaf path digestValue right)

linkTries :: Word64 -> OracleTrie -> Word64 -> OracleTrie -> Maybe OracleTrie
linkTries leftPath leftTrie rightPath rightTrie = do
  branchingBit <- highestDifferingBit leftPath rightPath
  let prefix = maskPrefix leftPath branchingBit
  pure
    ( if zeroAtBit leftPath branchingBit
        then oracleBranch prefix branchingBit leftTrie rightTrie
        else oracleBranch prefix branchingBit rightTrie leftTrie
    )

oracleBranch :: Word64 -> Word64 -> OracleTrie -> OracleTrie -> OracleTrie
oracleBranch prefix branchingBit left right =
  OracleBranch prefix branchingBit left right (branchDigest prefix branchingBit left right)

trieDigest :: OracleTrie -> OracleDigest
trieDigest trie =
  case trie of
    OracleEmpty -> oracleEmptyNodeDigest
    OracleLeaf _path digestValue -> digestValue
    OracleBranch _prefix _branchingBit _left _right digestValue -> digestValue

highestDifferingBit :: Word64 -> Word64 -> Maybe Word64
highestDifferingBit leftPath rightPath =
  let differentBits = leftPath `xor` rightPath
   in if differentBits == 0
        then Nothing
        else
          Just
            ( 1
                `shiftL` (finiteBitSize differentBits - countLeadingZeros differentBits - 1)
            )

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

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

digestWithKeys :: (Word64, Word64) -> (Word64, Word64) -> [Word8] -> OracleDigest
digestWithKeys lane0Key lane1Key bytes =
  OracleDigest (sipHash lane0Key bytes) (sipHash lane1Key bytes)

digestBytes :: OracleDigest -> [Word8]
digestBytes (OracleDigest lane0 lane1) =
  word64LE lane0 <> word64LE lane1

frameChunk :: [Word8] -> [Word8]
frameChunk bytes =
  compactWord64 (fromIntegral (length bytes)) <> bytes

word64LE :: Word64 -> [Word8]
word64LE word =
  fmap (fromIntegral . (word `shiftR`) . (* 8)) [0 .. 7]

packWord64 :: [Word8] -> Word64
packWord64 bytes =
  foldl'
    (.|.)
    0
    (zipWith (\shift byte -> fromIntegral byte `shiftL` shift) [0, 8 ..] bytes)

compactWord64 :: Word64 -> [Word8]
compactWord64 =
  unfoldr
    ( \maybeWord ->
        case maybeWord of
          Nothing -> Nothing
          Just word ->
            let byte = fromIntegral (word .&. 0x7f)
                remaining = word `shiftR` 7
             in Just
                  ( if remaining == 0 then byte else byte .|. 0x80,
                    if remaining == 0 then Nothing else Just remaining
                  )
    )
    . Just

sipHash :: (Word64, Word64) -> [Word8] -> Word64
sipHash (key0, key1) bytes =
  finalizeSipState
    (applySipRounds 4 (xorSecondStateWord 0xff absorbedFinalBlock))
  where
    initialState =
      SipState
        (key0 `xor` 0x736f6d6570736575)
        (key1 `xor` 0x646f72616e646f6d)
        (key0 `xor` 0x6c7967656e657261)
        (key1 `xor` 0x7465646279746573)
    chunks =
      unfoldr
        ( \remaining ->
            case remaining of
              [] -> Nothing
              _ -> Just (splitAt 8 remaining)
        )
        bytes
    fullBlocks = filter ((== 8) . length) chunks
    trailingBytes = foldMap (\chunk -> if length chunk < 8 then chunk else []) chunks
    absorbedBlocks = foldl' absorbBlock initialState (fmap packWord64 fullBlocks)
    finalBlock =
      (fromIntegral (length bytes) `shiftL` 56)
        .|. packWord64 trailingBytes
    absorbedFinalBlock = absorbBlock absorbedBlocks finalBlock

absorbBlock :: SipState -> Word64 -> SipState
absorbBlock (SipState v0 v1 v2 v3) block =
  case applySipRounds 2 (SipState v0 v1 v2 (v3 `xor` block)) of
    SipState r0 r1 r2 r3 -> SipState (r0 `xor` block) r1 r2 r3

applySipRounds :: Int -> SipState -> SipState
applySipRounds count initialState =
  foldl' (\state _round -> sipRound state) initialState [1 .. count]

sipRound :: SipState -> SipState
sipRound (SipState v0 v1 v2 v3) =
  SipState i0 i1 i2 i3
  where
    a0 = v0 + v1
    a1 = rotateL v1 13 `xor` a0
    a2 = rotateL a0 32
    b2 = v2 + v3
    b3 = rotateL v3 16 `xor` b2
    c0 = a2 + b3
    c3 = rotateL b3 21 `xor` c0
    d2 = b2 + a1
    d1 = rotateL a1 17 `xor` d2
    i0 = c0
    i1 = d1
    i2 = rotateL d2 32
    i3 = c3

xorSecondStateWord :: Word64 -> SipState -> SipState
xorSecondStateWord word (SipState v0 v1 v2 v3) =
  SipState v0 v1 (v2 `xor` word) v3

finalizeSipState :: SipState -> Word64
finalizeSipState (SipState v0 v1 v2 v3) =
  v0 `xor` v1 `xor` v2 `xor` v3

deltaHashProtocolVersion :: Word64
deltaHashProtocolVersion = 3

defaultLane0Key :: (Word64, Word64)
defaultLane0Key = (0x6d6f6f6e6c696768, 0x742d636f72652d31)

defaultLane1Key :: (Word64, Word64)
defaultLane1Key = (0x6d6f6f6e6c696768, 0x742d636f72652d32)

pathKey :: (Word64, Word64)
pathKey = (0x6d6c2d64656c7461, 0x706174682d763031)

emptyLane0Key :: (Word64, Word64)
emptyLane0Key = (0x6d6c2d64656c7461, 0x656d70742d763031)

emptyLane1Key :: (Word64, Word64)
emptyLane1Key = (0x6d6c2d64656c7461, 0x656d70742d763032)

leafLane0Key :: (Word64, Word64)
leafLane0Key = (0x6d6c2d64656c7461, 0x6c6561662d763031)

leafLane1Key :: (Word64, Word64)
leafLane1Key = (0x6d6c2d64656c7461, 0x6c6561662d763032)

branchLane0Key :: (Word64, Word64)
branchLane0Key = (0x6d6c2d64656c7461, 0x6272616e2d763031)

branchLane1Key :: (Word64, Word64)
branchLane1Key = (0x6d6c2d64656c7461, 0x6272616e2d763032)

multisetExpansionKey :: (Word64, Word64)
multisetExpansionKey = (0x6d6c2d64656c7461, 0x6d756c74692d7632)

multisetLane0Key :: (Word64, Word64)
multisetLane0Key = (0x6d6c2d64656c7461, 0x6d7365742d763031)

multisetLane1Key :: (Word64, Word64)
multisetLane1Key = (0x6d6c2d64656c7461, 0x6d7365742d763032)