haskoin-core-1.5.0: src/Haskoin/Transaction/Taproot.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE NoFieldSelectors #-}
-- |
-- Module : Haskoin.Transaction.Taproot
-- Copyright : No rights reserved
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- Taproot outputs, witnesses and signatures: the script trees and output
-- keys of BIP 341, the witness of a key path or script path spend, the
-- control block, and the digest a Taproot signature signs. For reference
-- see BIPs 340, 341 and 342.
module Haskoin.Transaction.Taproot
( -- * Keys
XOnlyPubKey (..),
keyParity,
-- * Script trees
TapLeafVersion,
tapscriptLeafVersion,
MAST (..),
mastCommitment,
getMerkleProofs,
leafHash,
TapLeaf (..),
tapTreeLeaves,
tapTreeFromLeaves,
-- * Outputs
TaprootOutput (..),
taprootOutputKey,
taprootOutputSecKey,
taprootScriptOutput,
-- * Witnesses
TaprootWitness (..),
ScriptPathData (..),
viewTaprootWitness,
encodeTaprootWitness,
verifyScriptPathData,
maxControlBlockNodes,
encodeControlBlock,
decodeControlBlock,
-- * Signatures
TaprootSignature (..),
isTaprootSigHash,
taprootSigHash,
encodeTaprootSig,
decodeTaprootSig,
)
where
import Control.Monad (forM_, unless, when, (<=<))
import Crypto.Hash
( Digest,
SHA256,
digestFromByteString,
hashFinalize,
hashUpdate,
hashUpdates,
)
import Crypto.Secp256k1
import Data.Aeson
( FromJSON (parseJSON),
ToJSON (toJSON),
Value (String),
withText,
)
import Data.Aeson.Types (Parser, Value)
import Data.Bits ((.&.), (.|.))
import Data.Bool (bool)
import Data.ByteArray qualified as BA
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Bytes.Get (MonadGet, getBytes, runGetS)
import Data.Bytes.Put (MonadPut, putByteString, putWord32le, putWord64le, putWord8, runPutL, runPutS)
import Data.Bytes.Serial (Serial (..), deserialize, serialize)
import Data.Bytes.VarInt (VarInt (VarInt))
import Data.Foldable (foldl')
import Data.Maybe (fromMaybe, maybeToList)
import Data.Word (Word32, Word8)
import Haskoin.Crypto.Hash
import Haskoin.Crypto.Keys.Common
import Haskoin.Network.Common (putVarIntBytes)
import Haskoin.Script.Common
import Haskoin.Script.SigHash
import Haskoin.Script.Standard
import Haskoin.Transaction.Common
import Haskoin.Util
{- Keys -}
-- | An x-only pubkey corresponds to the keys @(x,y)@ and @(x, -y)@. The
-- equality test only checks the x-coordinate. An x-only pubkey serializes to 32
-- bytes, and reads back as the point with the even y coordinate, as BIP 340
-- defines.
--
-- @since 0.21.0
newtype XOnlyPubKey = XOnlyPubKey {point :: PubKey}
deriving (Read, Show)
instance Eq XOnlyPubKey where
XOnlyPubKey k1 == XOnlyPubKey k2 = f k1 == f k2
where
f = BS.take 32 . (.get)
instance Marshal Ctx XOnlyPubKey where
marshalPut ctx (XOnlyPubKey pk) =
putByteString (exportXOnlyPubKey ctx pk)
marshalGet ctx =
maybe (fail "Not an x-only public key") (pure . XOnlyPubKey)
. importXOnlyPubKey ctx
=<< getBytes 32
instance MarshalJSON Ctx XOnlyPubKey where
unmarshalValue ctx =
withText "XOnlyPubKey" $ either fail pure . (des <=< hex)
where
hex = maybe (Left "Unable to decode hex") Right . decodeHex
des = runGetS $ marshalGet ctx
marshalValue ctx =
String . encodeHex . marshal ctx
marshalEncoding ctx =
hexEncoding . runPutL . marshalPut ctx
-- | The parity of the y coordinate of a key: 0 when it is even, 1 when it
-- is odd. It is the parity bit of a control block.
keyParity :: Ctx -> PubKey -> Word8
keyParity ctx key =
case BS.unpack . marshal ctx $ PublicKey key True of
0x02 : _ -> 0x00
_ -> 0x01
{- Script trees -}
-- | @since 0.21.0
type TapLeafVersion = Word8
-- | The leaf version of a Tapscript, @0xc0@, the only one BIP 342 defines.
tapscriptLeafVersion :: TapLeafVersion
tapscriptLeafVersion = 0xc0
-- | Merklized Abstract Syntax Tree. This type can represent trees where only a
-- subset of the leaves are known. Note that the tree is invariant under swapping
-- branches at an internal node.
--
-- @since 0.21.0
data MAST
= MASTBranch MAST MAST
| MASTLeaf TapLeafVersion Script
| MASTCommitment (Digest SHA256)
deriving (Eq, Show)
-- | Get the inclusion proofs for the leaves in the tree. The proof is ordered
-- leaf-to-root.
--
-- @since 0.21.0
getMerkleProofs :: MAST -> [(TapLeafVersion, Script, [Digest SHA256])]
getMerkleProofs = getProofs mempty
where
getProofs proof = \case
MASTBranch branchL branchR ->
(updateProof proof (mastCommitment branchR) <$> getMerkleProofs branchL)
<> (updateProof proof (mastCommitment branchL) <$> getMerkleProofs branchR)
MASTLeaf v s -> [(v, s, proof)]
MASTCommitment {} -> mempty
updateProof proofInit branchCommitment (v, s, proofTail) =
(v, s, reverse $ proofInit <> (branchCommitment : proofTail))
-- | Calculate the root hash for this tree.
--
-- @since 0.21.0
mastCommitment :: MAST -> Digest SHA256
mastCommitment = \case
MASTBranch leftBranch rightBranch ->
hashBranch (mastCommitment leftBranch) (mastCommitment rightBranch)
MASTLeaf leafVersion leafScript -> leafHash leafVersion leafScript
MASTCommitment theCommitment -> theCommitment
hashBranch :: Digest SHA256 -> Digest SHA256 -> Digest SHA256
hashBranch hashA hashB =
hashFinalize $
hashUpdates
(initTaggedHash "TapBranch")
[ min hashA hashB,
max hashA hashB
]
-- | The hash of a leaf of the tree, @TapLeaf@ of BIP 341: the tagged hash
-- of the leaf version and the script with its length.
leafHash :: TapLeafVersion -> Script -> Digest SHA256
leafHash leafVersion leafScript =
hashFinalize
. hashUpdate (initTaggedHash "TapLeaf")
. runPutS
$ do
serialize leafVersion
serialize $ VarInt (BS.length scriptBytes)
putByteString scriptBytes
where
scriptBytes = runPutS $ serialize leafScript
-- | A leaf of a script tree with its depth, as the field
-- @PSBT_OUT_TAP_TREE@ of BIP 371 lists the leaves, depth first
data TapLeaf = TapLeaf
{ depth :: !Word8,
version :: !TapLeafVersion,
script :: !Script
}
deriving (Eq, Show)
-- | The leaves of a tree, depth first, with their depth. 'Nothing' when a
-- branch of the tree is only known by its commitment, since its leaves are
-- not known.
tapTreeLeaves :: MAST -> Maybe [TapLeaf]
tapTreeLeaves = go 0
where
go d = \case
MASTBranch l r -> (<>) <$> go (d + 1) l <*> go (d + 1) r
MASTLeaf v s -> Just [TapLeaf d v s]
MASTCommitment _ -> Nothing
-- | The tree whose leaves these are, depth first with their depth, built
-- as @TaprootBuilder@ builds it in Bitcoin Core: two nodes at the same
-- depth join into their parent. 'Nothing' when the leaves do not make one
-- whole tree, or when a depth is above 128.
tapTreeFromLeaves :: [TapLeaf] -> Maybe MAST
tapTreeFromLeaves = go []
where
go :: [(Word8, MAST)] -> [TapLeaf] -> Maybe MAST
go [(0, tree)] [] = Just tree
go _ [] = Nothing
go stack (leaf : rest)
| leaf.depth > 128 = Nothing
| otherwise = go (merge ((leaf.depth, MASTLeaf leaf.version leaf.script) : stack)) rest
-- The newest node is first. Two nodes at the same depth become a
-- branch at the depth above, the older one on the left.
merge :: [(Word8, MAST)] -> [(Word8, MAST)]
merge ((d1, right) : (d2, left) : more)
| d1 == d2 && d1 > 0 = merge ((d1 - 1, MASTBranch left right) : more)
merge stack = stack
{- Outputs -}
-- | Representation of a full taproot output: the internal key and the
-- script tree, if there is one. Only the x coordinate of the internal key
-- matters, as BIP 341 defines the output from the x-only internal key: a
-- key and its negation give the same output.
--
-- @since 0.21.0
data TaprootOutput = TaprootOutput
{ internalKey :: PubKey,
mast :: Maybe MAST
}
-- | The output key of BIP 341: the x-only internal key tweaked with the
-- hash of the internal key and the root of the tree. The parity of its y
-- coordinate is the one a control block of a script path spend carries.
--
-- @since 0.21.0
taprootOutputKey :: Ctx -> TaprootOutput -> PubKey
taprootOutputKey ctx TaprootOutput {..} =
fromMaybe keyFail $
tweak commitment >>= tweakAddXOnlyPubKey ctx internalKey
where
commitment =
taprootCommitment ctx internalKey $
mastCommitment <$> mast
keyFail = error "haskoin-core taprootOutputKey: key derivation failed"
-- | The secret key that signs a key path spend of the output of the secret
-- key given: the secret key, negated when its public key has an odd y
-- coordinate, plus the tweak of 'taprootOutputKey'. 'Nothing' in the
-- negligible case where the tweak gives no key.
taprootOutputSecKey :: Ctx -> SecKey -> Maybe MAST -> Maybe SecKey
taprootOutputSecKey ctx secret mast =
tweak commitment >>= tweakAddXOnlySecKey ctx secret
where
commitment =
taprootCommitment ctx (derivePubKey ctx secret) $
mastCommitment <$> mast
taprootCommitment :: Ctx -> PubKey -> Maybe (Digest SHA256) -> ByteString
taprootCommitment ctx internalKey merkleRoot =
BA.convert
. hashFinalize
. maybe id (flip hashUpdate) merkleRoot
. (`hashUpdate` exportXOnlyPubKey ctx internalKey)
$ initTaggedHash "TapTweak"
-- | Generate the output script for a taproot output
--
-- @since 0.21.0
taprootScriptOutput :: Ctx -> TaprootOutput -> ScriptOutput
taprootScriptOutput ctx =
PayWitness 0x01
. runPutS
. marshalPut ctx
. XOnlyPubKey
. taprootOutputKey ctx
{- Witnesses -}
-- | Comprehension of taproot witness data
--
-- @since 0.21.0
data TaprootWitness
= -- | A key path spend: the signature, and the annex if there is one
KeyPathSpend ByteString (Maybe ByteString)
| ScriptPathSpend ScriptPathData
deriving (Eq, Show)
-- | A script path spend: the items the script runs on, the script, and
-- the control block that commits the script to the output key.
--
-- @since 0.21.0
data ScriptPathData = ScriptPathData
{ annex :: Maybe ByteString,
stack :: [ByteString],
script :: Script,
-- | the parity of the y coordinate of the output key
extIsOdd :: Bool,
-- | This value is masked by 0xFE
leafVersion :: Word8,
internalKey :: PubKey,
-- | the Merkle proof of the leaf, leaf to root
control :: [Digest SHA256]
}
deriving (Eq, Show)
-- | The number of nodes a control block may hold at most, the depth of the
-- deepest leaf BIP 341 allows
maxControlBlockNodes :: Int
maxControlBlockNodes = 128
-- | The control block of a script path spend: the leaf version with the
-- parity bit of the output key, the x-only internal key, and the proof
-- of the leaf
encodeControlBlock :: Ctx -> Word8 -> PubKey -> [Digest SHA256] -> ByteString
encodeControlBlock ctx versionAndParity internalKey proof =
BS.concat (BS.singleton versionAndParity : exportXOnlyPubKey ctx internalKey : map BA.convert proof)
-- | The parts of a control block, which must be 33 bytes plus 32 per node
-- of the proof, with at most 'maxControlBlockNodes' nodes, as BIP 341
-- requires
decodeControlBlock :: Ctx -> ByteString -> Either String (Word8, PubKey, [Digest SHA256])
decodeControlBlock ctx bytes
| len < 33 || (len - 33) `mod` 32 /= 0 =
Left ("decodeControlBlock: a control block is 33 bytes plus 32 per node, not " <> show len)
| nodeCount > maxControlBlockNodes =
Left ("decodeControlBlock: more than " <> show maxControlBlockNodes <> " nodes")
| otherwise = do
internalKey <- maybe (Left "decodeControlBlock: the internal key is not an x-only public key") Right $ importXOnlyPubKey ctx (BS.take 32 (BS.drop 1 bytes))
proof <- traverse node (chunks (BS.drop 33 bytes))
return (BS.head bytes, internalKey, proof)
where
len = BS.length bytes
nodeCount = (len - 33) `div` 32
chunks b
| BS.null b = []
| otherwise = BS.take 32 b : chunks (BS.drop 32 b)
node = maybe (Left "decodeControlBlock: not a hash") Right . digestFromByteString @SHA256
-- | Try to interpret a 'WitnessStack' as taproot witness data, as BIP 341
-- reads it: the last item is the annex when there are at least two items
-- and it starts with @0x50@; then one item left is a key path signature,
-- and more are the stack, the script and the control block.
--
-- @since 0.21.0
viewTaprootWitness :: Ctx -> WitnessStack -> Maybe TaprootWitness
viewTaprootWitness ctx witnessStack =
case reverse items of
[sig] -> Just (KeyPathSpend sig annex)
controlBytes : scriptBytes : reversedStack -> do
script <- eitherToMaybe $ runGetS deserialize scriptBytes
(v, internalKey, control) <- eitherToMaybe $ decodeControlBlock ctx controlBytes
pure $
ScriptPathSpend
ScriptPathData
{ annex,
stack = reverse reversedStack,
script,
extIsOdd = odd v,
leafVersion = v .&. 0xfe,
internalKey,
control
}
[] -> Nothing
where
(annex, items) = case reverse witnessStack of
a : rest
| not (null rest),
Just (0x50, _) <- BS.uncons a ->
(Just a, reverse rest)
_ -> (Nothing, witnessStack)
-- | Transform the high-level representation of taproot witness data into a witness stack
--
-- @since 0.21.0
encodeTaprootWitness :: Ctx -> TaprootWitness -> WitnessStack
encodeTaprootWitness ctx = \case
KeyPathSpend signature annex -> signature : maybeToList annex
ScriptPathSpend d ->
d.stack
<> [ runPutS (serialize d.script),
encodeControlBlock ctx (d.leafVersion .|. bool 0 1 d.extIsOdd) d.internalKey d.control
]
<> maybeToList d.annex
-- | Verify that the script path spend is valid, except for script execution:
-- the leaf and its proof commit to the output key, with the parity the
-- control block says.
--
-- @since 0.21.0
verifyScriptPathData ::
Ctx ->
-- | Output key
PubKey ->
ScriptPathData ->
Bool
verifyScriptPathData ctx outkey spd = fromMaybe False $ do
tweak commitment
>>= fmap onComputedKey
. tweakAddXOnlyPubKey ctx spd.internalKey
where
onComputedKey computedKey =
XOnlyPubKey outkey == XOnlyPubKey computedKey
&& expectedParity == keyParity ctx computedKey
commitment =
taprootCommitment ctx spd.internalKey (Just merkleRoot)
merkleRoot = foldl' hashBranch theLeafHash spd.control
theLeafHash = leafHash (spd.leafVersion .&. 0xfe) spd.script
expectedParity = bool 0 1 spd.extIsOdd
{- Signatures -}
-- | A Schnorr signature with the hash type it signs: 64 bytes, then the
-- hash type byte unless it is 'sigHashDefault'
data TaprootSignature = TaprootSignature
{ sig :: !SchnorrSig,
hash :: !SigHash
}
deriving (Eq, Show)
-- | Whether a hash type is one a Taproot signature may use: the default,
-- @ALL@, @NONE@ and @SINGLE@, the last three with or without
-- @ANYONECANPAY@. Bitcoin Core refuses every other value in
-- @SignatureHashSchnorr@.
isTaprootSigHash :: SigHash -> Bool
isTaprootSigHash sh = sh `elem` [0x00, 0x01, 0x02, 0x03, 0x81, 0x82, 0x83]
-- | The bytes of a Taproot signature as they go in a witness: the 64 bytes
-- of the signature, then the hash type byte unless it is 'sigHashDefault'
encodeTaprootSig :: TaprootSignature -> ByteString
encodeTaprootSig s
| s.hash == sigHashDefault = s.sig.get
| otherwise = s.sig.get <> BS.singleton (fromIntegral s.hash)
-- | A Taproot signature from its bytes: 64 bytes sign with
-- 'sigHashDefault', 65 end with a hash type byte that must be one
-- 'isTaprootSigHash' accepts and not zero, as BIP 341 requires
decodeTaprootSig :: ByteString -> Either String TaprootSignature
decodeTaprootSig bytes
| BS.length bytes == 64 = (`TaprootSignature` sigHashDefault) <$> sigOf bytes
| BS.length bytes == 65 = do
let sh = fromIntegral (BS.last bytes)
unless (isTaprootSigHash sh && sh /= sigHashDefault) $
Left ("decodeTaprootSig: invalid hash type byte " <> show (BS.last bytes))
(`TaprootSignature` sh) <$> sigOf (BS.init bytes)
| otherwise = Left ("decodeTaprootSig: a signature is 64 or 65 bytes, not " <> show (BS.length bytes))
where
sigOf = maybe (Left "decodeTaprootSig: not a Schnorr signature") Right . schnorrSig
-- | The digest a Taproot signature signs, as BIP 341 and BIP 342 define it:
-- the tagged hash of the signature message, which commits to the whole
-- transaction and to the output every input spends, and, for a script path
-- spend, to the leaf. Fails when the hash type is not one of
-- 'isTaprootSigHash', when the input index is out of range, when the
-- spent outputs do not match the inputs, or with @SIGHASH_SINGLE@ and no
-- output at the index of the input.
taprootSigHash ::
-- | transaction to sign
Tx ->
-- | outputs spent, one per input of the transaction, in order
[TxOut] ->
-- | index of input being signed
Int ->
-- | what to sign
SigHash ->
-- | annex of the input, if there is one
Maybe ByteString ->
-- | for a script path spend: the leaf hash, and the position of the last
-- @OP_CODESEPARATOR@ executed, @0xffffffff@ for none
Maybe (Digest SHA256, Word32) ->
Either String Hash256
taprootSigHash tx spent i sh annexM leafM = do
unless (i >= 0 && i < length tx.inputs) $
Left "taprootSigHash: input index out of range"
unless (length spent == length tx.inputs) $
Left "taprootSigHash: one spent output per input is required"
unless (isTaprootSigHash sh) $
Left ("taprootSigHash: invalid hash type " <> show (fromIntegral sh :: Word32))
when (isSigHashSingle sh && i >= length tx.outputs) $
Left "taprootSigHash: SIGHASH_SINGLE without an output at the input index"
return . taggedHash "TapSighash" . runPutS $ do
putWord8 0x00 -- the epoch
putWord8 (fromIntegral sh)
putWord32le tx.version
putWord32le tx.locktime
unless (anyoneCanPay sh) $ do
serialize $ sha256 (runPutS (mapM_ (serialize . (.outpoint)) tx.inputs))
serialize $ sha256 (runPutS (mapM_ (putWord64le . (.value)) spent))
serialize $ sha256 (runPutS (mapM_ (putVarIntBytes . (.script)) spent))
serialize $ sha256 (runPutS (mapM_ (putWord32le . (.sequence)) tx.inputs))
unless (isSigHashNone sh || isSigHashSingle sh) $
serialize $ sha256 (runPutS (mapM_ serialize tx.outputs))
putWord8 (bool 0 2 (isJust' leafM) .|. bool 0 1 (isJust' annexM))
if anyoneCanPay sh
then do
serialize input.outpoint
putWord64le coin.value
putVarIntBytes coin.script
putWord32le input.sequence
else putWord32le (fromIntegral i)
forM_ annexM $ \annex ->
serialize $ sha256 (runPutS (putVarIntBytes annex))
when (isSigHashSingle sh) $
serialize $ sha256 (runPutS (serialize (tx.outputs !! i)))
forM_ leafM $ \(leaf, codeSeparatorPosition) -> do
putByteString (BA.convert leaf)
putWord8 0x00 -- the key version
putWord32le codeSeparatorPosition
where
input = tx.inputs !! i
coin = spent !! i
isJust' = maybe False (const True)
-- | The tagged hash of BIP 340: SHA256 of the hash of the tag twice, then
-- the message
taggedHash :: ByteString -> ByteString -> Hash256
taggedHash tag message = sha256 (tagHash <> tagHash <> message)
where
tagHash = runPutS (serialize (sha256 tag))