Merge pull request #400 from GambolingPangolin/psbt-fixes

PSBT bug fixes
This commit is contained in:
Matthew Wraith 2021-10-20 13:51:57 -07:00 committed by GitHub
commit 000bded39c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 527 additions and 50 deletions

View File

@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Use a newtype for Fingerprint, which uses an 8 digit hex string for IsString,
Read, & Show. This fixes inconsistent (de)serialization across the package.
### Fixed
- Makes `finalScriptWitness` field encoding conform to bitcoin core.
- Fixes bug in `finalizeTransaction`
### Added
- Helper function for merging PSBTs
- Another PSBT test
## 0.20.5
### Added
- Support Bech32m address format for Taproot.

18
data/complex_psbt.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,10 +4,10 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
-- hash: 4b340a6dfe197ae20194205ecebc4358218dfb13d1451ff7606ed6290a4ba10e
-- hash: ec2010d735f2cc98f82ef204ad36efd8e1077e2f9cf9c67871c1e868c025a11f
name: haskoin-core
version: 0.20.5
version: 0.21.0
synopsis: Bitcoin & Bitcoin Cash library for Haskell
description: Please see the README on GitHub at <https://github.com/haskoin/haskoin-core#readme>
category: Bitcoin, Finance, Network
@ -21,6 +21,7 @@ license: MIT
license-file: LICENSE
build-type: Simple
extra-source-files:
data/complex_psbt.json
data/forkid_script_tests.json
data/forkid_sighash.json
data/key_io_invalid.json
@ -150,6 +151,7 @@ test-suite spec
, array >=0.5.4.0
, base >=4.9 && <5
, base16 >=0.3.0.1
, base64 ==0.4.*
, binary >=0.8.8
, bytes >=0.17
, bytestring >=0.10.10.0

View File

@ -1,5 +1,5 @@
name: haskoin-core
version: 0.20.5
version: 0.21.0
synopsis: Bitcoin & Bitcoin Cash library for Haskell
description: Please see the README on GitHub at <https://github.com/haskoin/haskoin-core#readme>
category: Bitcoin, Finance, Network
@ -63,6 +63,7 @@ tests:
build-tool-depends:
hspec-discover:hspec-discover
dependencies:
- base64 ^>= 0.4
- haskoin-core
- hspec >= 2.7.1
- HUnit >= 1.6.0.0

373
scripts/psbt_test.sh Executable file
View File

@ -0,0 +1,373 @@
#!/usr/bin/env bash
# Use Bitcoin Core 22.0 to create a complex PSBT test vector
if ! command -v bitcoind &> /dev/null; then
echo "Please install bitcoind"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "Please install jq"
exit 1
fi
datadir=$(mktemp -d "/tmp/bitcoind-regtest-XXXXXX")
btc () {
bitcoin-cli -regtest -datadir=$datadir "$@"
}
bitcoind -regtest -datadir=$datadir -fallbackfee=0.0001000 -daemon
while ! btc getblockchaininfo &> /dev/null; do
echo "Waiting for bitcoind"
sleep 5
done
passphrase="password"
create_wallet () {
btc -named createwallet wallet_name=$1 passphrase=$passphrase &> /dev/null
btc -rpcwallet=$1 walletpassphrase $passphrase 3600
}
get_address () {
btc -rpcwallet="miner" getnewaddress
}
get_priv_key () {
address=$(get_address)
btc -rpcwallet="miner" dumpprivkey $address
}
get_public_descriptor () {
btc getdescriptorinfo $1 | jq -r .descriptor
}
get_descriptor_address () {
btc deriveaddresses $1 | jq -r ".[0]"
}
get_canonical_descriptor () {
checksum=$(btc getdescriptorinfo "$1" | jq -r .checksum)
echo -n "$1#$checksum"
}
import_privkey () {
btc -rpcwallet=$1 importprivkey $2
}
import_descriptor () {
request=$(jq --null-input --arg "desc" $2 '[ { desc: $desc, timestamp: "now" } ]')
btc -rpcwallet=$1 importmulti "$request"
}
generate_blocks () {
btc generatetoaddress $1 $(btc -rpcwallet="miner" getnewaddress) &> /dev/null
}
sign_psbt () {
btc -rpcwallet=$1 -named walletprocesspsbt "psbt=$2" sign=true | jq -r .psbt
}
create_wallet "miner"
create_wallet "p2pkh"
create_wallet "p2sh-pk"
create_wallet "p2sh-ms-1"
create_wallet "p2sh-ms-2"
create_wallet "p2sh-ms-3"
create_wallet "p2sh-p2wpkh"
create_wallet "p2sh-wsh-pk"
create_wallet "p2sh-wsh-ms-1"
create_wallet "p2sh-wsh-ms-2"
create_wallet "p2sh-wsh-ms-3"
create_wallet "p2wpkh"
create_wallet "p2wsh-pk"
create_wallet "p2wsh-ms-1"
create_wallet "p2wsh-ms-2"
create_wallet "p2wsh-ms-3"
# Get an output to spend
miner_address=$(btc -rpcwallet="miner" getnewaddress)
generate_blocks 101
echo "Setting up p2pkh wallet"
setup_p2pkh () {
privkey=$(get_priv_key)
private_descriptor=$(get_canonical_descriptor "pkh($privkey)")
public_descriptor=$(get_public_descriptor $private_descriptor)
import_descriptor "p2pkh" "$private_descriptor" &> /dev/null
get_descriptor_address $public_descriptor
}
p2pkh_address=$(setup_p2pkh)
echo "Setting up p2sh-pk wallet"
setup_p2sh_p2pk () {
privkey=$(get_priv_key)
private_descriptor=$(get_canonical_descriptor "sh(pk($privkey))")
public_descriptor=$(get_public_descriptor $private_descriptor)
import_descriptor "p2sh-pk" "$private_descriptor" &> /dev/null
get_descriptor_address $public_descriptor
}
p2sh_pk_address=$(setup_p2sh_p2pk)
echo "Setting up p2sh-ms wallets"
setup_p2sh_ms () {
address1=$(get_address)
privkey1=$(btc -rpcwallet="miner" dumpprivkey $address1)
pubkey1=$(btc -rpcwallet="miner" getaddressinfo $address1 | jq -r .pubkey)
address2=$(get_address)
privkey2=$(btc -rpcwallet="miner" dumpprivkey $address2)
pubkey2=$(btc -rpcwallet="miner" getaddressinfo $address2 | jq -r .pubkey)
address3=$(get_address)
privkey3=$(btc -rpcwallet="miner" dumpprivkey $address3)
pubkey3=$(btc -rpcwallet="miner" getaddressinfo $address3 | jq -r .pubkey)
private_descriptor_1=$(get_canonical_descriptor "sh(sortedmulti(2,$privkey1,$pubkey2,$pubkey3))")
import_descriptor "p2sh-ms-1" "$private_descriptor_1" &> /dev/null
private_descriptor_2=$(get_canonical_descriptor "sh(sortedmulti(2,$pubkey1,$privkey2,$pubkey3))")
import_descriptor "p2sh-ms-2" "$private_descriptor_2" &> /dev/null
private_descriptor_3=$(get_canonical_descriptor "sh(sortedmulti(2,$pubkey1,$pubkey2,$privkey3))")
import_descriptor "p2sh-ms-3" "$private_descriptor_3" &> /dev/null
public_descriptor=$(get_public_descriptor $private_descriptor_1)
get_descriptor_address $public_descriptor
}
p2sh_ms_address=$(setup_p2sh_ms)
echo "Setting up p2sh-wsh-pk wallet"
setup_p2sh_wsh_pk () {
privkey=$(get_priv_key)
private_descriptor=$(get_canonical_descriptor "sh(wsh(pk($privkey)))")
public_descriptor=$(get_public_descriptor $private_descriptor)
import_descriptor "p2sh-wsh-pk" "$private_descriptor" &> /dev/null
get_descriptor_address $public_descriptor
}
p2sh_wsh_pk_address=$(setup_p2sh_wsh_pk)
echo "Setting up p2sh-wsh-ms wallets"
setup_p2sh_wsh_ms () {
address1=$(get_address)
privkey1=$(btc -rpcwallet="miner" dumpprivkey $address1)
pubkey1=$(btc -rpcwallet="miner" getaddressinfo $address1 | jq -r .pubkey)
address2=$(get_address)
privkey2=$(btc -rpcwallet="miner" dumpprivkey $address2)
pubkey2=$(btc -rpcwallet="miner" getaddressinfo $address2 | jq -r .pubkey)
address3=$(get_address)
privkey3=$(btc -rpcwallet="miner" dumpprivkey $address3)
pubkey3=$(btc -rpcwallet="miner" getaddressinfo $address3 | jq -r .pubkey)
private_descriptor_1=$(get_canonical_descriptor "sh(wsh(sortedmulti(2,$privkey1,$pubkey2,$pubkey3)))")
import_descriptor "p2sh-wsh-ms-1" "$private_descriptor_1" &> /dev/null
private_descriptor_2=$(get_canonical_descriptor "sh(wsh(sortedmulti(2,$pubkey1,$privkey2,$pubkey3)))")
import_descriptor "p2sh-wsh-ms-2" "$private_descriptor_2" &> /dev/null
private_descriptor_3=$(get_canonical_descriptor "sh(wsh(sortedmulti(2,$pubkey1,$pubkey2,$privkey3)))")
import_descriptor "p2sh-wsh-ms-3" "$private_descriptor_3" &> /dev/null
public_descriptor=$(get_public_descriptor $private_descriptor_1)
get_descriptor_address $public_descriptor
}
p2sh_wsh_ms_address=$(setup_p2sh_wsh_ms)
echo "Setting up p2wpkh wallet"
setup_p2wpkh () {
privkey=$(get_priv_key)
private_descriptor=$(get_canonical_descriptor "wpkh($privkey)")
public_descriptor=$(get_public_descriptor $private_descriptor)
import_descriptor "p2wpkh" "$private_descriptor" &> /dev/null
get_descriptor_address $public_descriptor
}
p2wpkh_address=$(setup_p2wpkh)
echo "Setting up p2wsh-pk wallet"
setup_p2wsh_pk () {
privkey=$(get_priv_key)
private_descriptor=$(get_canonical_descriptor "wsh(pk($privkey))")
public_descriptor=$(get_public_descriptor $private_descriptor)
import_descriptor "p2wsh-pk" "$private_descriptor" &> /dev/null
get_descriptor_address $public_descriptor
}
p2wsh_pk_address=$(setup_p2wsh_pk)
echo "Setting up p2wsh-ms wallets"
setup_p2wsh_ms () {
address1=$(get_address)
privkey1=$(btc -rpcwallet="miner" dumpprivkey $address1)
pubkey1=$(btc -rpcwallet="miner" getaddressinfo $address1 | jq -r .pubkey)
address2=$(get_address)
privkey2=$(btc -rpcwallet="miner" dumpprivkey $address2)
pubkey2=$(btc -rpcwallet="miner" getaddressinfo $address2 | jq -r .pubkey)
address3=$(get_address)
privkey3=$(btc -rpcwallet="miner" dumpprivkey $address3)
pubkey3=$(btc -rpcwallet="miner" getaddressinfo $address3 | jq -r .pubkey)
private_descriptor_1=$(get_canonical_descriptor "wsh(sortedmulti(2,$privkey1,$pubkey2,$pubkey3))")
import_descriptor "p2wsh-ms-1" "$private_descriptor_1" &> /dev/null
private_descriptor_2=$(get_canonical_descriptor "wsh(sortedmulti(2,$pubkey1,$privkey2,$pubkey3))")
import_descriptor "p2wsh-ms-2" "$private_descriptor_2" &> /dev/null
private_descriptor_3=$(get_canonical_descriptor "wsh(sortedmulti(2,$pubkey1,$pubkey2,$privkey3))")
import_descriptor "p2wsh-ms-3" "$private_descriptor_3" &> /dev/null
public_descriptor=$(get_public_descriptor $private_descriptor_1)
get_descriptor_address $public_descriptor
}
p2wsh_ms_address=$(setup_p2wsh_ms)
addresses=(
$p2pkh_address
$p2sh_pk_address
$p2sh_ms_address
$p2sh_wsh_pk_address
$p2sh_wsh_ms_address
$p2wpkh_address
$p2wsh_pk_address
$p2wsh_ms_address
)
fund_addresses () {
jq_entries="[]"
for addr in "${addresses[@]}"; do
jq_entries=$(echo $jq_entries | jq --arg addr $addr '. += [ { key: $addr, value: 0.5 } ]')
done
amounts=$(jq '. | from_entries' <<< $jq_entries)
btc -rpcwallet="miner" -named sendmany "amounts=$amounts"
}
funding_txid=$(fund_addresses)
funding_tx_outputs=$(
btc -named getrawtransaction "txid=$funding_txid" verbose=true | jq -r '.vout | length - 1'
)
create_psbt () {
psbt_inputs="[]"
for i in $(seq 0 $funding_tx_outputs); do
psbt_inputs=$(
echo $psbt_inputs |
jq \
--arg txid $funding_txid \
--arg vout $i \
'. += [ { txid: $txid, vout: $vout | tonumber } ]'
)
done
psbt_outputs=$(jq --null-input --arg addr $miner_address '[ { ($addr): 3.99 } ]')
btc -named createpsbt "inputs=$psbt_inputs" "outputs=$psbt_outputs"
}
psbt=$(create_psbt)
miner_psbt=$(sign_psbt "miner" "$psbt")
p2pkh_psbt=$(sign_psbt "p2pkh" "$psbt")
p2sh_pk_psbt=$(sign_psbt "p2sh-pk" "$psbt")
p2sh_ms_1_psbt=$(sign_psbt "p2sh-ms-1" "$psbt")
p2sh_ms_2_psbt=$(sign_psbt "p2sh-ms-2" "$psbt")
p2sh_wsh_pk_psbt=$(sign_psbt "p2sh-wsh-pk" "$psbt")
p2sh_wsh_ms_1_psbt=$(sign_psbt "p2sh-wsh-ms-1" "$psbt")
p2sh_wsh_ms_2_psbt=$(sign_psbt "p2sh-wsh-ms-2" "$psbt")
p2wpkh_psbt=$(sign_psbt "p2wpkh" "$psbt")
p2wsh_pk_psbt=$(sign_psbt "p2wsh-pk" "$psbt")
p2wsh_ms_1_psbt=$(sign_psbt "p2wsh-ms-1" "$psbt")
p2wsh_ms_2_psbt=$(sign_psbt "p2wsh-ms-2" "$psbt")
signed_psbts=(
$miner_psbt
$p2pkh_psbt
$p2sh_pk_psbt
$p2sh_ms_1_psbt
$p2sh_ms_2_psbt
$p2sh_wsh_pk_psbt
$p2sh_wsh_ms_1_psbt
$p2sh_wsh_ms_2_psbt
$p2wpkh_psbt
$p2wsh_pk_psbt
$p2wsh_ms_1_psbt
$p2wsh_ms_2_psbt
)
psbt_list="[]"
for signed_psbt in "${signed_psbts[@]}"; do
psbt_list=$(
echo $psbt_list |
jq --arg signed_psbt $signed_psbt '. += [ $signed_psbt ]'
)
done
combined_psbt=$(btc combinepsbt "$psbt_list")
complete_psbt=$(btc -named finalizepsbt "psbt=$combined_psbt" extract=false | jq -r .psbt)
final_tx=$(btc -named finalizepsbt "psbt=$combined_psbt" extract=true | jq -r .hex)
summary_entries="[]"
add_summary_field () {
summary_entries=$(
echo $summary_entries |
jq --arg key $1 --arg value $2 '. += [ { key: $key, value: $value } ]'
)
}
add_summary_field "initial_psbt" $psbt
add_summary_field "miner_psbt" $miner_psbt
add_summary_field "p2pkh_psbt" $p2pkh_psbt
add_summary_field "p2sh_pk_psbt" $p2sh_pk_psbt
add_summary_field "p2sh_ms_1_psbt" $p2sh_ms_1_psbt
add_summary_field "p2sh_ms_2_psbt" $p2sh_ms_2_psbt
add_summary_field "p2sh_wsh_pk_psbt" $p2sh_wsh_pk_psbt
add_summary_field "p2sh_wsh_ms_1_psbt" $p2sh_wsh_ms_1_psbt
add_summary_field "p2sh_wsh_ms_2_psbt" $p2sh_wsh_ms_2_psbt
add_summary_field "p2wpkh_psbt" $p2wpkh_psbt
add_summary_field "p2wsh_pk_psbt" $p2wsh_pk_psbt
add_summary_field "p2wsh_ms_1_psbt" $p2wsh_ms_1_psbt
add_summary_field "p2wsh_ms_2_psbt" $p2wsh_ms_2_psbt
add_summary_field "combined_psbt" $combined_psbt
add_summary_field "complete_psbt" $complete_psbt
add_summary_field "final_tx" $final_tx
echo $summary_entries | jq '. | from_entries' > /tmp/psbt_vectors.json
echo "Done."
btc stop
rm -r $datadir

View File

@ -21,6 +21,7 @@ module Haskoin.Transaction.Partial
, UnknownMap (..)
, Key (..)
, merge
, mergeMany
, mergeInput
, mergeOutput
, complete
@ -32,7 +33,7 @@ module Haskoin.Transaction.Partial
import Control.Applicative ((<|>))
import Control.DeepSeq
import Control.Monad (guard, replicateM, void)
import Control.Monad (guard, replicateM, void, foldM)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Bytes.Get (runGetS)
@ -137,6 +138,13 @@ merge psbt1 psbt2
}
merge _ _ = Nothing
-- | A version of 'merge' for a collection of PSBTs.
--
-- @since 0.21.0
mergeMany :: [PartiallySignedTransaction] -> Maybe PartiallySignedTransaction
mergeMany (psbt : psbts) = foldM merge psbt psbts
mergeMany _ = Nothing
mergeInput :: Input -> Input -> Input
mergeInput a b = Input
{ nonWitnessUtxo =
@ -206,7 +214,19 @@ complete psbt =
outputScript = eitherToMaybe . decodeOutputBS . scriptOutput
completeInput (Nothing, input) = input
completeInput (Just script, input) = completeSig input script
completeInput (Just script, input) = pruneInputFields $ completeSig input script
-- If we have final scripts, we can get rid of data for signing following
-- the Bitcoin Core implementation.
pruneInputFields input
| isJust (finalScriptSig input) || isJust (finalScriptWitness input) =
input { partialSigs = mempty
, inputHDKeypaths = mempty
, inputRedeemScript = Nothing
, inputWitnessScript = Nothing
, sigHashType = Nothing
}
| otherwise = input
indexed :: [a] -> [(Word32, a)]
indexed = zip [0..]
@ -236,34 +256,31 @@ completeSig input (PayPKHash h)
completeSig input (PayMulSig pubKeys m)
| length sigs >= m =
input { finalScriptSig = finalSig }
input { finalScriptSig = Just finalSig }
where
sigs = collectSigs m pubKeys input
finalSig =
Script .
(OP_0 :) .
(map opPushData sigs <>) .
pure . opPushData . runPutS . serialize <$>
inputRedeemScript input
finalSig = Script $ OP_0 : map opPushData sigs
completeSig input (PayScriptHash h)
| Just rdmScript <- inputRedeemScript input
, PayScriptHash h == toP2SH rdmScript
, Right decodedScript <- decodeOutput rdmScript
, not (isPayScriptHash decodedScript) =
completeSig input decodedScript
pushScript rdmScript $ completeSig input decodedScript
where
pushScript rdmScript updatedInput =
updatedInput
{ finalScriptSig = Just $
fromMaybe (Script mempty) (finalScriptSig updatedInput)
`scriptAppend` serializedRedeemScript rdmScript
}
scriptAppend (Script script1) (Script script2) = Script $ script1 <> script2
completeSig input (PayWitnessPKHash h)
| [(k, sig)] <- HashMap.toList (partialSigs input)
, PubKeyAddress h == pubKeyAddr k =
input
{
finalScriptWitness =
Just [sig, runPutS $ serialize k],
finalScriptSig =
Script . pure . opPushData . runPutS . serialize <$>
inputRedeemScript input
}
input {finalScriptWitness = Just [sig, runPutS $ serialize k]}
completeSig input (PayWitnessScriptHash h)
| Just witScript <- inputWitnessScript input
, PayWitnessScriptHash h == toP2WSH witScript
@ -272,18 +289,15 @@ completeSig input (PayWitnessScriptHash h)
completeSig input _ = input
serializedRedeemScript :: Script -> Script
serializedRedeemScript = Script . pure . opPushData . runPutS . serialize
completeWitnessSig :: Input -> ScriptOutput -> Input
completeWitnessSig input script@(PayMulSig pubKeys m)
| length sigs >= m =
input
{
finalScriptWitness = Just finalWit,
finalScriptSig = finalSig
}
input {finalScriptWitness = Just finalWit}
where
sigs = collectSigs m pubKeys input
finalSig = Script . pure . opPushData . runPutS . serialize <$>
inputRedeemScript input
finalWit = mempty : sigs <> [encodeOutputBS script]
completeWitnessSig input _ = input
@ -317,19 +331,10 @@ finalTransaction psbt =
txWitness = if hasWitness then reverse witData else []
}
finalizeInput (ins, witData) (txInput, psbtInput) =
maybe finalWitness finalScript $
finalScriptSig psbtInput
where
finalScript script =
(
txInput { scriptInput = runPutS $ serialize script } : ins,
[] : witData
)
finalWitness =
(
ins,
fromMaybe [] (finalScriptWitness psbtInput) : witData
)
(
txInput { scriptInput = maybe mempty (runPutS . serialize) $ finalScriptSig psbtInput } : ins,
fromMaybe [] (finalScriptWitness psbtInput) : witData
)
-- | Take an unsigned transaction and produce an empty
-- 'PartiallySignedTransaction'
@ -476,7 +481,7 @@ instance Serialize Input where
putHDPath InBIP32Derivation inputHDKeypaths
whenJust (putKeyValue InFinalScriptSig . serialize)
finalScriptSig
whenJust (putKeyValue InFinalScriptWitness . serialize)
whenJust (putKeyValue InFinalScriptWitness . putFinalScriptWitness)
finalScriptWitness
S.put inputUnknown
S.putWord8 0x00
@ -487,6 +492,9 @@ instance Serialize Input where
putKey InSigHashType
S.putWord8 0x04
S.putWord32le (fromIntegral sigHash)
putFinalScriptWitness witnessStack = do
S.put $ (VarInt . fromIntegral . length) witnessStack
mapM_ (serialize . VarString) witnessStack
instance Serialize Output where
get = getMap getOutputItem setOutputUnknown emptyOutput
@ -630,8 +638,8 @@ getInputItem 0 input@Input{finalScriptWitness = Nothing} InFinalScriptWitness =
scripts <- map getVarString <$> getVarIntList
return $ input { finalScriptWitness = Just scripts }
where
getVarIntList = do
VarInt n <- deserialize
getVarIntList = getSizedBytes $ do
VarInt n <- deserialize -- Item count
replicateM (fromIntegral n) deserialize
getInputItem keySize input inputType = fail $

View File

@ -2,7 +2,6 @@
module Haskoin.Transaction.PartialSpec (spec) where
import Control.Monad.Fail (MonadFail)
import Data.ByteString (ByteString)
import Data.Bytes.Get
import Data.Bytes.Put
@ -17,15 +16,22 @@ import Test.HUnit (Assertion, assertBool,
import Test.Hspec
import Test.QuickCheck
import Control.Monad ((<=<))
import Data.Aeson (FromJSON, parseJSON, withObject,
(.:))
import Data.Bifunctor (first)
import Data.ByteString.Base64 (decodeBase64)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8)
import Haskoin.Address
import Haskoin.Constants
import Haskoin.Crypto
import Haskoin.Keys
import Haskoin.Script
import Haskoin.Transaction
import Haskoin.Transaction.Partial
import Haskoin.Util
import Haskoin.Util.Arbitrary
import Haskoin.UtilSpec (readTestFile)
spec :: Spec
spec = describe "partially signed bitcoin transaction unit tests" $ do
@ -50,6 +56,10 @@ spec = describe "partially signed bitcoin transaction unit tests" $ do
forAll arbitraryKeyPair $ verifyNonWitnessPSBT btc . unfinalizedPkhPSBT btc
it "signed and finalized multisig PSBTs verify" $ property $
forAll arbitraryMultiSig $ verifyNonWitnessPSBT btc . unfinalizedMsPSBT btc
it "encodes and decodes psbt with final witness script" $
(fmap (encodeHex . S.encode) . decodeHexPSBT) validVec7Hex == Right validVec7Hex
it "handles complex psbts correctly" complexPsbtTest
vec2Test :: Assertion
vec2Test = do
@ -203,6 +213,26 @@ vec6Test = do
(Key 0x0f (fromJust $ decodeHex "010203040506070809"))
(fromJust $ decodeHex "0102030405060708090a0b0c0d0e0f")
complexPsbtTest :: Assertion
complexPsbtTest = do
complexPsbtData <- readTestFile "complex_psbt.json"
let computedCombinedPsbt = mergeMany $ complexSignedPsbts complexPsbtData
expectedCombinedPsbt = stripRedundantUtxos $ complexCombinedPsbt complexPsbtData
assertEqual "combined psbt" computedCombinedPsbt (Just expectedCombinedPsbt)
let computedCompletePsbt = complete $ complexCombinedPsbt complexPsbtData
expectedCompletePsbt = complexCompletePsbt complexPsbtData
assertEqual "complete psbt" computedCompletePsbt expectedCompletePsbt
let computedFinalTx = finalTransaction $ complexCompletePsbt complexPsbtData
assertEqual "final tx" computedFinalTx (complexFinalTx complexPsbtData)
where
stripRedundantUtxos psbt = psbt { inputs = stripRedundantUtxo <$> inputs psbt }
stripRedundantUtxo input
| Just{} <- witnessUtxo input = input { nonWitnessUtxo = Nothing }
| otherwise = input
expectedOut :: ScriptOutput
expectedOut = fromRight (error "could not decode expected output")
. decodeOutputBS . fromJust $ decodeHex "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787"
@ -408,3 +438,40 @@ validVec5Hex = "70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e33
validVec6Hex :: Text
validVec6Hex = "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000"
-- Example of a PSBT with a `finalWitnessScript`
validVec7Hex :: Text
validVec7Hex = "70736274ff0100520200000001815dd29e16fd2f567a040ce24f5337fb9cfd0c05bacd8890714a33edc7cbbc920000000000ffffffff0192f1052a01000000160014ef9ade26f63015d57f4ecdb268d1a9b8d6cd8872000000000001008402000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0200f2052a010000001600145f4ffa19dbbe464561c50fc4d8d8ac0b41009dd20000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90000000001011f00f2052a010000001600145f4ffa19dbbe464561c50fc4d8d8ac0b41009dd201086b02473044022026a9f7afdb0128970bb3577e536ec3d3dc10c1e82650d11c9da1df9003b31d0202202258b11f962f12e0897c642cd6f38a0181db17197f3693a530c9431eb44dde7d0121033dc786e9628bb6c41c08fceb9b37458ad7a95e7e6b04e0bde45b6879398c3ac100220203a6affb58dda998a4ffdce652feb91038fdfc78c748ae687372e11292af8d312d101c4c5bfc00000080000000800100008000"
data ComplexPsbtData = ComplexPsbtData
{ complexSignedPsbts :: [ PartiallySignedTransaction ]
, complexCombinedPsbt :: PartiallySignedTransaction
, complexCompletePsbt :: PartiallySignedTransaction
, complexFinalTx :: Tx
}
deriving (Eq, Show)
instance FromJSON ComplexPsbtData where
parseJSON = withObject "ComplexPsbtData" $ \obj -> do
ComplexPsbtData
<$> sequence
[ psbtField "miner_psbt" obj
, psbtField "p2pkh_psbt" obj
, psbtField "p2sh_ms_1_psbt" obj
, psbtField "p2sh_ms_2_psbt" obj
, psbtField "p2sh_pk_psbt" obj
, psbtField "p2sh_wsh_pk_psbt" obj
, psbtField "p2sh_wsh_ms_1_psbt" obj
, psbtField "p2sh_wsh_ms_2_psbt" obj
, psbtField "p2wpkh_psbt" obj
, psbtField "p2wsh_pk_psbt" obj
, psbtField "p2wsh_ms_1_psbt" obj
, psbtField "p2wsh_ms_2_psbt" obj
]
<*> psbtField "combined_psbt" obj
<*> psbtField "complete_psbt" obj
<*> (obj .: "final_tx" >>= parseTx)
where
parseTx = either fail pure . (S.decode <=< maybe (Left "hex") Right . decodeHex)
parsePsbt = either fail pure . (S.decode <=< first Text.unpack . decodeBase64) . encodeUtf8
psbtField fieldName obj = obj .: fieldName >>= parsePsbt

View File

@ -74,7 +74,7 @@ customCerealID :: Eq a => Get a -> Putter a -> a -> Bool
customCerealID g p a = runGet g (runPut (p a)) == Right a
readTestFile :: A.FromJSON a => FilePath -> IO a
readTestFile fp = do
fileM <- A.decodeFileStrict $ "data/" <> fp
maybe (error $ "Could not read test file " <> fp) return fileM
readTestFile fp =
A.eitherDecodeFileStrict ("data/" <> fp) >>= either (error . message) return
where
message aesonErr = "Could not read test file " <> fp <> ": " <> aesonErr