mirror of
https://github.com/haskoin/haskoin-core.git
synced 2024-09-11 10:05:34 +03:00
Merge pull request #400 from GambolingPangolin/psbt-fixes
PSBT bug fixes
This commit is contained in:
commit
000bded39c
@ -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
18
data/complex_psbt.json
Normal file
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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
373
scripts/psbt_test.sh
Executable 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
|
@ -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 $
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user