haskoin-core-0.21.0: test/Haskoin/Transaction/TaprootSpec.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Haskoin.Transaction.TaprootSpec (spec) where
import Control.Applicative ((<|>))
import Control.Monad (zipWithM, (<=<))
import Data.Aeson (FromJSON (parseJSON), withObject, (.:), (.:?))
import Data.Aeson.Types (Parser)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Bytes.Get (runGetS)
import Data.Bytes.Put (runPutS)
import Data.Bytes.Serial (deserialize, serialize)
import Data.Text (Text)
import Data.Word (Word8)
import Haskoin (
MAST (..),
PubKey,
PubKeyI (PubKeyI),
ScriptOutput,
ScriptPathData (..),
TaprootOutput (TaprootOutput),
TaprootWitness (ScriptPathSpend),
XOnlyPubKey (..),
addrToText,
btc,
decodeHex,
encodeTaprootWitness,
getMerkleProofs,
mastCommitment,
outputAddress,
taprootInternalKey,
taprootMAST,
taprootOutputKey,
taprootScriptOutput,
verifyScriptPathData,
)
import Haskoin.UtilSpec (readTestFile)
import Test.HUnit (assertBool, (@?=))
import Test.Hspec (Spec, describe, it, runIO)
spec :: Spec
spec = do
TestVector{testScriptPubKey} <- runIO $ readTestFile "bip341.json"
describe "Taproot" $ do
it "should calculate the correct hashes" $ mapM_ testHashes testScriptPubKey
it "should build the correct output key" $ mapM_ testOutputKey testScriptPubKey
it "should build the correct script output" $ mapM_ testScriptOutput testScriptPubKey
it "should calculate the correct control blocks" $ mapM_ testControlBlocks testScriptPubKey
it "should arrive at the correct address" $ mapM_ testAddress testScriptPubKey
testHashes :: TestScriptPubKey -> IO ()
testHashes testData =
mapM_ checkMASTDetails $ (taprootMAST . tspkGiven) testData
where
checkMASTDetails theMAST = do
-- Leaf hashes
(Just . getLeafHashes) theMAST @?= (spkiLeafHashes . tspkIntermediary) testData
-- Merkle root
(Just . BA.convert . mastCommitment) theMAST @?= (spkiMerkleRoot . tspkIntermediary) testData
getLeafHashes = \case
MASTBranch branchL branchR -> getLeafHashes branchL <> getLeafHashes branchR
leaf@MASTLeaf{} -> [BA.convert $ mastCommitment leaf]
MASTCommitment{} -> mempty -- The test vectors have complete trees
testOutputKey :: TestScriptPubKey -> IO ()
testOutputKey testData = do
XOnlyPubKey (taprootOutputKey theOutput) @?= theOutputKey
where
theOutput = tspkGiven testData
theOutputKey = XOnlyPubKey . spkiTweakedPubKey $ tspkIntermediary testData
testScriptOutput :: TestScriptPubKey -> IO ()
testScriptOutput testData =
taprootScriptOutput (tspkGiven testData) @?= (spkeScriptPubKey . tspkExpected) testData
testControlBlocks :: TestScriptPubKey -> IO ()
testControlBlocks testData = do
mapM_ onExamples exampleControlBlocks
mapM_ checkVerification scriptPathSpends
where
theOutput = tspkGiven testData
theOutputKey = taprootOutputKey theOutput
exampleControlBlocks = spkeControlBlocks $ tspkExpected testData
calculatedControlBlocks =
(!! 1) . encodeTaprootWitness . ScriptPathSpend <$> scriptPathSpends
scriptPathSpends =
fmap mkScriptPathSpend
. maybe mempty getMerkleProofs
$ taprootMAST theOutput
mkScriptPathSpend (scriptPathLeafVersion, scriptPathScript, proof) =
ScriptPathData
{ scriptPathAnnex = Nothing
, scriptPathStack = mempty
, scriptPathScript
, scriptPathExternalIsOdd = odd $ keyParity theOutputKey
, scriptPathLeafVersion
, scriptPathInternalKey = taprootInternalKey theOutput
, scriptPathControl = BA.convert <$> proof
}
onExamples = zipWithM (@?=) calculatedControlBlocks
checkVerification = assertBool "Script verifies" . verifyScriptPathData theOutputKey
keyParity :: PubKey -> Word8
keyParity key = case BS.unpack . runPutS . serialize $ PubKeyI key True of
0x02 : _ -> 0x00
_ -> 0x01
testAddress :: TestScriptPubKey -> IO ()
testAddress testData = computedAddress @?= (Just . spkeAddress . tspkExpected) testData
where
computedAddress = (addrToText btc <=< outputAddress) . taprootScriptOutput $ tspkGiven testData
newtype SpkGiven = SpkGiven {unSpkGiven :: TaprootOutput}
instance FromJSON SpkGiven where
parseJSON = withObject "SpkGiven" $ \obj ->
fmap SpkGiven $
TaprootOutput
<$> (xOnlyPubKey <$> obj .: "internalPubkey")
<*> (obj .:? "scriptTree" >>= traverse parseScriptTree)
where
parseScriptTree v =
parseScriptLeaf v
<|> parseScriptBranch v
<|> fail "Unable to parse scriptTree"
parseScriptLeaf = withObject "ScriptTree leaf" $ \obj ->
MASTLeaf
<$> obj .: "leafVersion"
<*> (obj .: "script" >>= hexScript)
parseScriptBranch v =
parseJSON v >>= \case
[v1, v2] -> MASTBranch <$> parseScriptTree v1 <*> parseScriptTree v2
_ -> fail "ScriptTree branch"
hexScript = either fail pure . runGetS deserialize <=< jsonHex
data SpkIntermediary = SpkIntermediary
{ spkiLeafHashes :: Maybe [ByteString]
, spkiMerkleRoot :: Maybe ByteString
, spkiTweakedPubKey :: PubKey
}
instance FromJSON SpkIntermediary where
parseJSON = withObject "SpkIntermediary" $ \obj ->
SpkIntermediary
<$> (obj .:? "leafHashes" >>= (traverse . traverse) jsonHex)
<*> (obj .: "merkleRoot" >>= traverse jsonHex)
<*> (xOnlyPubKey <$> obj .: "tweakedPubkey")
data SpkExpected = SpkExpected
{ spkeScriptPubKey :: ScriptOutput
, spkeControlBlocks :: Maybe [ByteString]
, spkeAddress :: Text
}
instance FromJSON SpkExpected where
parseJSON = withObject "SpkExpected" $ \obj ->
SpkExpected
<$> obj .: "scriptPubKey"
<*> (obj .:? "scriptPathControlBlocks" >>= (traverse . traverse) jsonHex)
<*> obj .: "bip350Address"
data TestScriptPubKey = TestScriptPubKey
{ tspkGiven :: TaprootOutput
, tspkIntermediary :: SpkIntermediary
, tspkExpected :: SpkExpected
}
instance FromJSON TestScriptPubKey where
parseJSON = withObject "TestScriptPubKey" $ \obj ->
TestScriptPubKey
<$> (unSpkGiven <$> obj .: "given")
<*> obj .: "intermediary"
<*> obj .: "expected"
newtype TestVector = TestVector
{ testScriptPubKey :: [TestScriptPubKey]
}
instance FromJSON TestVector where
parseJSON = withObject "TestVector" $ \obj ->
TestVector <$> obj .: "scriptPubKey"
jsonHex :: Text -> Parser ByteString
jsonHex = maybe (fail "Unable to decode hex") pure . decodeHex