packages feed

sparse-merkle-trees-0.2.0.0: src/Crypto/Hash/CompactSparseMerkleTree.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}

-- |
-- Module      : Crypto.Hash.CompactSparseMerkleTree
-- Description : Compact sparse merkle trees
-- Copyright   : (c) Tochi Obudulu 2022
-- License     : BSD-3
-- Maintainer  : tochicool@gmail.com
-- Portability : portable
-- Stability   : experimental
--
--
-- = Compact Sparse Merkle Trees
--
-- The @'CompactSparseMerkleTree' i alg a@ type represents a merkle tree of size
-- @i@ containing elements of type @a@ authenticated with a secure cryptographic
-- hash function @alg@. This allows for the novel generation and verification of
-- memory efficient cryptographic zero-knowledge proofs of inclusion and
-- /exclusion/ of elements in the tree. Most operations require that @a@ be an
-- instance of the 'ByteArrayAccess' class and @alg@ be an instance of the
-- 'HashAlgorithm' class.
--
-- This module is intended to be imported qualified:
--
-- >  import Crypto.Hash.CompactSparseMerkleTree (CSMT)
-- >  import qualified Crypto.Hash.CompactSparseMerkleTree as CSMT
--
--
-- == Warning
--
-- The size of the tree obviously cannot exceed the size of the image of the
-- hash algorithm @2^(8 * hashDigestSize alg)@. The word length of the hash
-- digest for the algorithm must not exceed @maxBound :: Int@. Violation of
-- these limits are not detected and a breach implies undefined behaviour.
--
--
-- == Implementation
--
-- The implementation of 'CompactSparseMerkleTree' is based on /compact/ sparse
-- merkle trees as described by:
--
--    * Faraz Haider. "Compact sparse merkle trees.",
--      Cryptology ePrint Archive, October 2018,
--      <https://eprint.iacr.org/2018/955>.
--
-- Asymptotic bounds for the average case time complexity are given with the
-- assumption that the supplied hash function acts as a random oracle under the
-- random oracle model and that the compact sparse merkle tree is 'valid'. In
-- practice, the probability that the observed complexity differs from the
-- average case is vanishingly small.
--
-- Additionally, this implementation enforces /domain separation/ for the inputs
-- to the hash algorithm @alg@ to provide the proofs with resistance to second
-- preimage attacks. Inputs to hashes for leaf and parent nodes are prefixed
-- with the bytes @0x00@ and @0x01@ respectively before applying the hash
-- algorithm.
module Crypto.Hash.CompactSparseMerkleTree
  ( -- * CompactSparseMerkleTree Type
    CSMT,
    CompactSparseMerkleTree (..),
    Size (..),

    -- * Construction
    empty,
    singleton,
    fromList,

    -- * Insertion
    insert,

    -- * Deletion
    delete,

    -- * Query
    lookup,
    member,
    notMember,

    -- * Min\/Max
    minimumDigest,
    maximumDigest,

    -- * Proofs
    MembershipProof (..),
    Proof (..),
    Direction (..),
    ProofType (..),
    isInclusionProof,
    isExclusionProof,

    -- ** Proof construction
    membershipProof,

    -- ** Proof verification
    MerkleRoot (..),
    merkleRoot,
    validProof,
    validInclusionProof,
    validExclusionProof,
    valid,

    -- * Debugging
    depth,
    toTree,
    drawTree,
  )
where

import Crypto.Hash (Digest, HashAlgorithm, hashFinalize, hashInit, hashUpdate, hashUpdates)
import Crypto.Hash.CompactSparseMerkleTree.DataNode (DataNode)
import qualified Crypto.Hash.CompactSparseMerkleTree.DataNode as DN
import Data.Bifunctor (first)
import Data.Bits (FiniteBits (countLeadingZeros))
import Data.ByteArray (ByteArrayAccess)
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS
import Data.Foldable (foldl', toList)
import Data.Functor (void)
import Data.Functor.Classes (Eq1 (liftEq), Ord1 (liftCompare))
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Maybe (mapMaybe)
import Data.Ord (comparing)
import Data.Tree (Tree)
import qualified Data.Tree as Tree
import Prelude hiding (lookup)

-- | A compact sparse merkle tree of size @i@ with values @a@ authenticated over
-- the algorithm @alg@.
type CSMT = CompactSparseMerkleTree

-- | A compact sparse merkle tree of size @i@ with values @a@ authenticated over
-- the algorithm @alg@.
data CompactSparseMerkleTree (i :: Size) alg a where
  -- | The empty tree.
  Nil :: CSMT 'Empty alg a
  -- | A leaf node.
  Leaf ::
    { -- | The hash digest of the data element.
      digest :: Digest alg,
      -- | The data value.
      value :: a
    } ->
    CSMT 'NonEmpty alg a
  -- | A parent node.
  Parent ::
    { -- | The left non-empty subtree.
      left :: CSMT 'NonEmpty alg a,
      -- | The hash digest of the concatenation of the left and right subtree digests.
      digest :: Digest alg,
      -- | The maximum digest in the tree.
      maxDigest :: Digest alg,
      -- | The right non-empty subtree.
      right :: CSMT 'NonEmpty alg a
    } ->
    CSMT 'NonEmpty alg a

-- | The size of a compact sparse merkle tree.
data Size
  = -- | The empty tree
    Empty
  | -- | A non-empty tree
    NonEmpty

deriving instance (Show a) => Show (CSMT i alg a)

deriving instance Foldable (CSMT i alg)

deriving instance (Eq a) => Eq (CSMT i alg a)

deriving instance (Ord a) => Ord (CSMT i alg a)

instance Eq1 (CSMT i alg) where
  liftEq eq m n = liftEq eq (toList m) (toList n)

instance Ord1 (CSMT i alg) where
  liftCompare cmp m n = liftCompare cmp (toList m) (toList n)

-- | The empty tree.
--
-- Worst case Θ(1).
empty :: CSMT 'Empty alg a
empty = Nil

-- | Create a singleton tree.
--
-- Worst case Θ(1).
singleton :: (ByteArrayAccess a, HashAlgorithm alg) => a -> CSMT 'NonEmpty alg a
singleton x = singletonDigest (hashLeaf x) x

singletonDigest :: Digest alg -> a -> CSMT 'NonEmpty alg a
singletonDigest h x = Leaf {digest = h, value = x}

-- | Insert an element in a tree. If the tree already contains an element whose
-- hash is equal to the given value, it is replaced with the new value.
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
insert :: (ByteArrayAccess a, HashAlgorithm alg) => a -> CSMT i alg a -> CSMT 'NonEmpty alg a
insert x = insertDigest (hashLeaf x) x

insertDigest :: (ByteArrayAccess a, HashAlgorithm alg) => Digest alg -> a -> CSMT i alg a -> CSMT 'NonEmpty alg a
insertDigest h x = \case
  Nil -> singletonDigest h x
  root@Leaf {} ->
    let newLeaf = singletonDigest h x
     in case h `compare` maximumDigest root of
          LT -> parent newLeaf root
          EQ -> singletonDigest h x
          GT -> parent root newLeaf
  root@Parent {left, right} ->
    case compareSubTrees h left right of
      EQ ->
        let newLeaf = singletonDigest h x
            minKey = min (maximumDigest left) (maximumDigest right)
         in case h `compare` minKey of
              LT -> parent newLeaf root
              __ -> parent root newLeaf
      LT -> parent (insertDigest h x left) right
      GT -> parent left (insertDigest h x right)

-- | Delete an element from a tree if such an element exists.
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
delete :: (ByteArrayAccess a, HashAlgorithm alg) => a -> CSMT i alg a -> (forall j. CSMT j alg a -> b) -> b
delete x = deleteDigest (hashLeaf x)

deleteDigest :: HashAlgorithm alg => Digest alg -> CSMT i alg a -> (forall j. CSMT j alg a -> b) -> b
deleteDigest h root returnTree = case root of
  Nil {} -> returnTree root
  Leaf {digest}
    | h == digest -> returnTree empty
    | otherwise -> returnTree root
  Parent {left, right} ->
    case compareSubTrees h left right of
      EQ -> returnTree root
      LT -> deleteDigest h left $ \case
        Nil {} -> returnTree right
        left'@Leaf {} -> returnTree $ parent left' right
        left'@Parent {} -> returnTree $ parent left' right
      GT -> deleteDigest h right $ \case
        Nil {} -> returnTree left
        right'@Leaf {} -> returnTree $ parent left right'
        right'@Parent {} -> returnTree $ parent left right'

compareSubTrees :: Digest alg -> CSMT 'NonEmpty alg a -> CSMT 'NonEmpty alg a -> Ordering
compareSubTrees h = comparing (distance h . maximumDigest)

distance :: Digest alg -> Digest alg -> Int
distance a = logBase2 . BA.xor a
  where
    logBase2 x =
      8 * BS.length x - 1 - case span (== 0) (BS.unpack x) of
        (zeros, nonZeros) ->
          8 * length zeros + case nonZeros of
            [] -> 0
            (w : _) -> countLeadingZeros w

-- | The maximum digest in the tree.
--
-- Worst case Θ(1).
maximumDigest :: CSMT 'NonEmpty alg a -> Digest alg
maximumDigest Leaf {digest} = digest
maximumDigest Parent {maxDigest} = maxDigest

-- | The minimum digest in the tree.
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
minimumDigest :: CSMT 'NonEmpty alg a -> Digest alg
minimumDigest = \case
  Leaf {digest} -> digest
  Parent {left} -> minimumDigest left

parent :: HashAlgorithm alg => CSMT 'NonEmpty alg a -> CSMT 'NonEmpty alg a -> CSMT 'NonEmpty alg a
parent left right =
  Parent
    { left,
      digest = hashParent (digest left) (digest right),
      maxDigest = max (maximumDigest left) (maximumDigest right),
      right
    }

hashLeaf :: (HashAlgorithm a, ByteArrayAccess ba) => ba -> Digest a
hashLeaf = hashFinalize . hashUpdate (hashUpdate hashInit (BS.singleton 0))

hashParent :: (HashAlgorithm a, ByteArrayAccess ba) => ba -> ba -> Digest a
hashParent x y = hashFinalize $ hashUpdates (hashUpdate hashInit (BS.singleton 1)) [x, y]

--------------------------------------------------------------------------------

-- | A membership proof over a hash algorithm @alg@.
data MembershipProof alg = forall p. MembershipProof (Proof Direction p alg)

-- | A proof of @p@ with direction @d@ over a hash algorithm @alg@.
data Proof d (p :: ProofType) alg where
  -- | A proof of inclusion.
  InclusionProof ::
    { -- | A digest of an included element.
      includedDigest :: Digest alg,
      -- | A list of sibling digests from the root to the included element with the directions from their parents.
      rootPath :: [(Digest alg, d)]
    } ->
    Proof d 'Inclusion alg
  -- | A proof of exclusion.
  ExclusionProof ::
    { -- | The digest of an excluded element.
      excludedDigest :: Digest alg,
      -- | A uni-directional inclusion proof from the left of the immediate predecessor to the included element, if one exists.
      immediatePredecessor :: Maybe (Proof () 'Inclusion alg),
      -- | A uni-directional inclusion proof from the right of the immediate successor to the included element, if one exists.
      immediateSuccessor :: Maybe (Proof () 'Inclusion alg),
      -- | A list of sibling digests from the root to the first common sibling of the immediate predecessor and successors with the directions from their parents.
      commonRootPath :: [(Digest alg, d)]
    } ->
    Proof d 'Exclusion alg

deriving instance Show d => Show (Proof d alg p)

-- | A direction of a node from its parent.
data Direction
  = -- | A left node
    L
  | -- | A right node
    R
  deriving (Show, Eq)

deriving instance Show (MembershipProof alg)

-- | A type of proof
data ProofType
  = -- | A proof that an element is in a tree.
    Inclusion
  | -- | A proof that an element is not in a tree.
    Exclusion

-- | Is the element in the tree?
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
member :: (ByteArrayAccess a, HashAlgorithm alg) => a -> CSMT i alg a -> Bool
member x = isInclusionProof . membershipProof x

-- | Is the element not in the tree?
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
notMember :: (ByteArrayAccess a, HashAlgorithm alg) => a -> CSMT i alg a -> Bool
notMember x = not . member x

-- | Lookup the value with the digest in the map.
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
lookup :: Digest alg -> CSMT i alg a -> Maybe a
lookup h = \case
  Nil {} -> Nothing
  Leaf {digest, value}
    | h == digest -> Just value
    | otherwise -> Nothing
  Parent {left, right} ->
    case compareSubTrees h left right of
      EQ -> Nothing
      LT -> lookup h left
      GT -> lookup h right

-- | Is this an inclusion proof?
--
-- Worst case Θ(1).
isInclusionProof :: MembershipProof alg -> Bool
isInclusionProof = \case
  MembershipProof (InclusionProof {}) -> True
  _ -> False

-- | Is this an exclusion proof?
--
-- Worst case Θ(1).
isExclusionProof :: MembershipProof alg -> Bool
isExclusionProof = not . isInclusionProof

-- | Construct a membership proof of inclusion if the given element is in the
-- tree, or a proof of exclusion if the element is not in the tree.
--
-- Average case Θ(log n), Worst case Θ(n) where n is the size of the tree.
-- The constructed membership proof has equivalent space complexity.
membershipProof :: (ByteArrayAccess a, HashAlgorithm alg) => a -> CSMT i alg a -> MembershipProof alg
membershipProof x = membershipProofDigest [] (hashLeaf x)

membershipProofDigest :: [(CSMT 'NonEmpty alg a, Direction)] -> Digest alg -> CSMT i alg a -> MembershipProof alg
membershipProofDigest path h = \case
  Nil {} ->
    MembershipProof $ trivialExclusionProof h
  leaf@Leaf {digest} -> case h `compare` digest of
    EQ ->
      MembershipProof (trivialInclusionProof digest) {rootPath = toRootPath path}
    __ -> nonMembershipProof path h leaf
  root@Parent {left, right} ->
    case compareSubTrees h left right of
      EQ -> case h `compare` maximumDigest root of
        LT -> nonMembershipProof ((right, R) : path) h left
        __ -> nonMembershipProof ((left, L) : path) h right
      LT -> membershipProofDigest ((right, R) : path) h left
      GT -> membershipProofDigest ((left, L) : path) h right

nonMembershipProof :: [(CSMT 'NonEmpty alg a, Direction)] -> Digest alg -> CSMT 'NonEmpty alg a -> MembershipProof alg
nonMembershipProof path h t =
  let exclusionProof = trivialExclusionProof h
   in case h `compare` maximumDigest t of
        LT ->
          case spanDirection R path of
            (successorPath, []) ->
              MembershipProof $
                exclusionProof
                  { immediateSuccessor = Just $ minimumDigestInclusionProof' successorPath t
                  }
            (successorPath, (sibling, _) : commonPath) ->
              MembershipProof $
                exclusionProof
                  { immediateSuccessor = Just $ minimumDigestInclusionProof' successorPath t,
                    immediatePredecessor = Just $ maximumDigestInclusionProof sibling,
                    commonRootPath = toRootPath commonPath
                  }
        __ ->
          case spanDirection L path of
            (predecessorPath, []) ->
              MembershipProof $
                exclusionProof
                  { immediatePredecessor = Just $ maximumDigestInclusionProof' predecessorPath t
                  }
            (predecessorPath, (sibling, _) : commonPath) ->
              MembershipProof $
                exclusionProof
                  { immediatePredecessor = Just $ maximumDigestInclusionProof' predecessorPath t,
                    immediateSuccessor = Just $ minimumDigestInclusionProof sibling,
                    commonRootPath = toRootPath commonPath
                  }

spanDirection :: Eq d => d -> [(CSMT 'NonEmpty alg a, d)] -> ([(Digest alg, ())], [(CSMT 'NonEmpty alg a, d)])
spanDirection d = first toUniRootPath . span ((d ==) . snd)

toUniRootPath :: [(CSMT 'NonEmpty alg a, d)] -> [(Digest alg, ())]
toUniRootPath = fmap void . toRootPath

toRootPath :: [(CSMT 'NonEmpty alg a, d)] -> [(Digest alg, d)]
toRootPath = fmap (first digest)

trivialInclusionProof :: Digest alg -> Proof d 'Inclusion alg
trivialInclusionProof h =
  InclusionProof
    { includedDigest = h,
      rootPath = mempty
    }

trivialExclusionProof :: Digest alg -> Proof d 'Exclusion alg
trivialExclusionProof h =
  ExclusionProof
    { excludedDigest = h,
      commonRootPath = [],
      immediatePredecessor = Nothing,
      immediateSuccessor = Nothing
    }

maximumDigestInclusionProof :: CSMT 'NonEmpty alg a -> Proof () 'Inclusion alg
maximumDigestInclusionProof = maximumDigestInclusionProof' []

maximumDigestInclusionProof' :: [(Digest alg, ())] -> CSMT 'NonEmpty alg a -> Proof () 'Inclusion alg
maximumDigestInclusionProof' path = \case
  Leaf {digest} -> (trivialInclusionProof digest) {rootPath = path}
  Parent {left, right} -> maximumDigestInclusionProof' ((digest left, ()) : path) right

minimumDigestInclusionProof :: CSMT 'NonEmpty alg a -> Proof () 'Inclusion alg
minimumDigestInclusionProof = minimumDigestInclusionProof' []

minimumDigestInclusionProof' :: [(Digest alg, ())] -> CSMT 'NonEmpty alg a -> Proof () 'Inclusion alg
minimumDigestInclusionProof' path = \case
  Leaf {digest} -> (trivialInclusionProof digest) {rootPath = path}
  Parent {left, right} -> minimumDigestInclusionProof' ((digest right, ()) : path) left

--------------------------------------------------------------------------------

-- | A merkle root of a tree.
data MerkleRoot alg
  = -- | A merkle root of an empty tree.
    EmptyMerkleRoot
  | -- | A merkle root of a non-empty tree.
    MerkleRoot (Digest alg)
  deriving (Show, Eq)

-- | The merkle root of a tree.
--
-- Worst case Θ(1).
merkleRoot :: CSMT i alg a -> MerkleRoot alg
merkleRoot = \case
  Nil {} -> EmptyMerkleRoot
  Leaf {digest} -> MerkleRoot digest
  Parent {digest} -> MerkleRoot digest

-- | Validate a membership proof against a merkle root.
--
-- Worst case Θ(d) where d is the number of hash digests in the membership proof.
validProof :: HashAlgorithm alg => MerkleRoot alg -> MembershipProof alg -> Bool
validProof root = \case
  MembershipProof proof@InclusionProof {} -> validInclusionProof root proof
  MembershipProof proof@ExclusionProof {} -> validExclusionProof root proof

-- | Validate an inclusion proof against a merkle root.
--
-- Worst case Θ(d) where d is the number of hash digests in the inclusion proof.
validInclusionProof :: HashAlgorithm alg => MerkleRoot alg -> Proof Direction 'Inclusion alg -> Bool
validInclusionProof EmptyMerkleRoot _ = False
validInclusionProof (MerkleRoot root) proof = root == inclusionProofMerkleRoot proof

inclusionProofMerkleRoot :: HashAlgorithm alg => Proof Direction 'Inclusion alg -> Digest alg
inclusionProofMerkleRoot InclusionProof {includedDigest, rootPath} =
  foldl'
    ( \result (siblingDigest, direction) -> uncurry hashParent $
        case direction of
          L -> (siblingDigest, result)
          R -> (result, siblingDigest)
    )
    includedDigest
    rootPath

-- | Validate an exclusion proof against a merkle root.
--
-- Worst case Θ(d) where d is the number of hash digests in the exclusion proof.
validExclusionProof :: HashAlgorithm alg => MerkleRoot alg -> Proof Direction 'Exclusion alg -> Bool
validExclusionProof root = \case
  ExclusionProof {immediatePredecessor = Nothing, immediateSuccessor = Nothing} ->
    root == EmptyMerkleRoot
  ExclusionProof {immediatePredecessor = Just p, excludedDigest, immediateSuccessor = Nothing}
    | includedDigest p < excludedDigest ->
      validInclusionProof root $ mapProofDirection (const L) p
  ExclusionProof {immediatePredecessor = Nothing, excludedDigest, immediateSuccessor = Just q}
    | excludedDigest < includedDigest q ->
      validInclusionProof root $ mapProofDirection (const R) q
  ExclusionProof {immediatePredecessor = Just p, commonRootPath, excludedDigest, immediateSuccessor = Just q}
    | includedDigest p < excludedDigest,
      excludedDigest < includedDigest q ->
      let leftMerkleRoot = inclusionProofMerkleRoot $ mapProofDirection (const L) p
          rightMerkleRoot = inclusionProofMerkleRoot $ mapProofDirection (const R) q
          includedDigest = hashParent leftMerkleRoot rightMerkleRoot
       in validInclusionProof root $
            InclusionProof
              { includedDigest,
                rootPath = commonRootPath
              }
  _ -> False

mapProofDirection :: (d -> d') -> Proof d 'Inclusion alg -> Proof d' 'Inclusion alg
mapProofDirection f proof@InclusionProof {rootPath} = proof {rootPath = fmap (fmap f) rootPath}

-- | Validate a tree against the properties of a compact sparse merkle tree. Namely that:
--
-- * the maximum leaf digests for all subtrees are valid
-- * the leaf hash digests are valid
-- * and all leafs lie on its /minimum distance path/ from the root.
--
-- All exported functions maintain these properties.
--
-- Average case Θ(n*log n), Worst case Θ(n²) where n is the size of the tree.
valid :: (ByteArrayAccess a, HashAlgorithm alg) => CSMT i alg a -> Bool
valid = valid' (const True)
  where
    valid' :: (ByteArrayAccess a, HashAlgorithm alg) => (Digest alg -> Bool) -> CSMT i alg a -> Bool
    valid' validPath = \case
      Nil {} -> True
      Leaf {digest, value} -> hashLeaf value == digest && validPath digest
      Parent {left, digest = parentDigest, maxDigest, right} ->
        maximumDigest left <= maxDigest
          && maximumDigest right <= maxDigest
          && parentDigest == hashParent (digest left) (digest right)
          && valid' (\h -> compareSubTrees h left right == LT && validPath h) left
          && valid' (\h -> compareSubTrees h left right == GT && validPath h) right

-- | Create a tree from a list of elements.
--
-- Average case Θ(n*log n), Worst case Θ(n²) where n is the size of the tree.
fromList :: (ByteArrayAccess a, HashAlgorithm alg) => NonEmpty a -> CSMT 'NonEmpty alg a
fromList = \case
  (x :| []) -> singleton x
  (x :| y : ys) -> insert x $ fromList (y :| ys)

--------------------------------------------------------------------------------

-- | The depth of a tree.
--
-- Average case Θ(n), Worst case Θ(n) where n is the size of the tree.
depth :: (Num n, Ord n) => CSMT i alg a -> n
depth = \case
  Nil -> 0
  Leaf {} -> 1
  Parent {left, right} -> 1 + max (depth left) (depth right)

-- | Convert a tree to a rose tree with non-recursive nodes as elements.
-- Used for debugging purposes.
--
-- Average case Θ(n), Worst case Θ(n) where n is the size of the tree.
toTree :: CSMT i alg a -> Maybe (Tree (DataNode alg a))
toTree = \case
  Nil -> Nothing
  Leaf {digest, value} ->
    Just $
      Tree.Node
        ( DN.ExternalNode
            { digest,
              value
            }
        )
        []
  Parent {left, digest, maxDigest, right} ->
    Just $
      Tree.Node
        ( DN.InternalNode
            { digest,
              maxDigest
            }
        )
        $ mapMaybe toTree [left, right]

-- | 2-dimensional ASCII drawing of the tree.
-- Used for debugging purposes.
--
-- Average case Θ(n²), Worst case Θ(n²) where n is the size of the tree.
drawTree :: Show a => CSMT i alg a -> String
drawTree = maybe "" (Tree.drawTree . fmap show) . toTree