qa: functional tests lianad using Taproot descriptors
We introduce Taproot support in the test framework through a global toggle. A few modifications are made to some tests to adapt them under Taproot (notably the hardcoded fees / amounts). This is based on my introduction of a quick and dirty support for TapMiniscript in my python-bip380 library: https://github.com/darosior/python-bip380/pull/23. In addition to this i didn't want to implement a signer in the Python test suite so here we introduce a simple Rust program based on our "hot signer" which will sign a PSBT with an xpriv provided through its stdin and output the signed PSBT on its stdout. Eventually it would be nicer to have a Python signer instead of having to call a program. The whole test suite should pass under both Taproot and P2WSH. Only a single test is skipped for now under Taproot since it needs a finalizer in the test suite. I also caught a bug in the RBF tests which i fixed in place.
This commit is contained in:
parent
96d30db5b8
commit
ecef6bff5e
@ -44,6 +44,16 @@ From the root of the repository:
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
For running the tests under Taproot a `bitcoind` version 26.0 or superior must be used. It can be
|
||||
pointed to using the `BITCOIND_PATH` variable. For now, one must also compile the `taproot_signer`
|
||||
Rust program:
|
||||
```
|
||||
(cd tests/tools/taproot_signer && cargo build --release)
|
||||
```
|
||||
|
||||
Then the test suite can be run by using Taproot descriptors instead of P2WSH descriptors by setting
|
||||
the `USE_TAPROOT` environment variable to `1`.
|
||||
|
||||
### Tips and tricks
|
||||
#### Logging
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ from concurrent import futures
|
||||
from test_framework.bitcoind import Bitcoind
|
||||
from test_framework.lianad import Lianad
|
||||
from test_framework.signer import SingleSigner, MultiSigner
|
||||
from test_framework.utils import (
|
||||
EXECUTOR_WORKERS,
|
||||
)
|
||||
from test_framework.utils import EXECUTOR_WORKERS, USE_TAPROOT
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import pytest
|
||||
import shutil
|
||||
@ -120,22 +119,36 @@ def xpub_fingerprint(hd):
|
||||
return _pubkey_to_fingerprint(hd.pubkey).hex()
|
||||
|
||||
|
||||
def single_key_desc(prim_fg, prim_xpub, reco_fg, reco_xpub, csv_value, is_taproot):
|
||||
if is_taproot:
|
||||
return f"tr([{prim_fg}]{prim_xpub}/<0;1>/*,and_v(v:pk([{reco_fg}]{reco_xpub}/<0;1>/*),older({csv_value})))"
|
||||
else:
|
||||
return f"wsh(or_d(pk([{prim_fg}]{prim_xpub}/<0;1>/*),and_v(v:pkh([{reco_fg}]{reco_xpub}/<0;1>/*),older({csv_value}))))"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lianad(bitcoind, directory):
|
||||
datadir = os.path.join(directory, "lianad")
|
||||
os.makedirs(datadir, exist_ok=True)
|
||||
bitcoind_cookie = os.path.join(bitcoind.bitcoin_dir, "regtest", ".cookie")
|
||||
|
||||
signer = SingleSigner()
|
||||
signer = SingleSigner(is_taproot=USE_TAPROOT)
|
||||
(prim_fingerprint, primary_xpub), (reco_fingerprint, recovery_xpub) = (
|
||||
(xpub_fingerprint(signer.primary_hd), signer.primary_hd.get_xpub()),
|
||||
(xpub_fingerprint(signer.recovery_hd), signer.recovery_hd.get_xpub()),
|
||||
)
|
||||
csv_value = 10
|
||||
# NOTE: origins are the actual xpub themselves which is incorrect but make it
|
||||
# NOTE: origins are the actual xpub themselves which is incorrect but makes it
|
||||
# possible to differentiate them.
|
||||
main_desc = Descriptor.from_str(
|
||||
f"wsh(or_d(pk([{prim_fingerprint}]{primary_xpub}/<0;1>/*),and_v(v:pkh([{reco_fingerprint}]{recovery_xpub}/<0;1>/*),older({csv_value}))))"
|
||||
single_key_desc(
|
||||
prim_fingerprint,
|
||||
primary_xpub,
|
||||
reco_fingerprint,
|
||||
recovery_xpub,
|
||||
csv_value,
|
||||
is_taproot=USE_TAPROOT,
|
||||
)
|
||||
)
|
||||
|
||||
lianad = Lianad(
|
||||
@ -156,8 +169,19 @@ def lianad(bitcoind, directory):
|
||||
lianad.cleanup()
|
||||
|
||||
|
||||
def multi_expression(thresh, keys):
|
||||
exp = f"multi({thresh},"
|
||||
def unspendable_internal_xpub(xpubs):
|
||||
"""Deterministic, unique, unspendable internal key.
|
||||
See See https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21
|
||||
"""
|
||||
chaincode = hashlib.sha256(b"".join(xpub.pubkey for xpub in xpubs)).digest()
|
||||
bip341_nums = bytes.fromhex(
|
||||
"0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
|
||||
)
|
||||
return BIP32(chaincode, pubkey=bip341_nums, network="test")
|
||||
|
||||
|
||||
def multi_expression(thresh, keys, is_taproot):
|
||||
exp = f"multi_a({thresh}," if is_taproot else f"multi({thresh},"
|
||||
for i, key in enumerate(keys):
|
||||
# NOTE: origins are the actual xpub themselves which is incorrect but make it
|
||||
# possible to differentiate them.
|
||||
@ -168,6 +192,21 @@ def multi_expression(thresh, keys):
|
||||
return exp + ")"
|
||||
|
||||
|
||||
def multisig_desc(multi_signer, csv_value, is_taproot):
|
||||
prim_multi, recov_multi = (
|
||||
multi_expression(3, multi_signer.prim_hds, is_taproot),
|
||||
multi_expression(2, multi_signer.recov_hds[csv_value], is_taproot),
|
||||
)
|
||||
if is_taproot:
|
||||
all_xpubs = [
|
||||
hd for hd in multi_signer.prim_hds + multi_signer.recov_hds[csv_value]
|
||||
]
|
||||
internal_key = unspendable_internal_xpub(all_xpubs).get_xpub()
|
||||
return f"tr([00000000]{internal_key}/<0;1>/*,{{{prim_multi},and_v(v:{recov_multi},older({csv_value}))}})"
|
||||
else:
|
||||
return f"wsh(or_d({prim_multi},and_v(v:{recov_multi},older({csv_value}))))"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lianad_multisig(bitcoind, directory):
|
||||
datadir = os.path.join(directory, "lianad")
|
||||
@ -176,13 +215,9 @@ def lianad_multisig(bitcoind, directory):
|
||||
|
||||
# A 3-of-4 that degrades into a 2-of-5 after 10 blocks
|
||||
csv_value = 10
|
||||
signer = MultiSigner(4, {csv_value: 5})
|
||||
prim_multi, recov_multi = (
|
||||
multi_expression(3, signer.prim_hds),
|
||||
multi_expression(2, signer.recov_hds[csv_value]),
|
||||
)
|
||||
signer = MultiSigner(4, {csv_value: 5}, is_taproot=USE_TAPROOT)
|
||||
main_desc = Descriptor.from_str(
|
||||
f"wsh(or_d({prim_multi},and_v(v:{recov_multi},older({csv_value}))))"
|
||||
multisig_desc(signer, csv_value, is_taproot=USE_TAPROOT)
|
||||
)
|
||||
|
||||
lianad = Lianad(
|
||||
@ -203,6 +238,28 @@ def lianad_multisig(bitcoind, directory):
|
||||
lianad.cleanup()
|
||||
|
||||
|
||||
def multipath_desc(multi_signer, csv_values, is_taproot):
|
||||
prim_multi = multi_expression(3, multi_signer.prim_hds, is_taproot)
|
||||
first_recov_multi = multi_expression(
|
||||
3, multi_signer.recov_hds[csv_values[0]], is_taproot
|
||||
)
|
||||
second_recov_multi = multi_expression(
|
||||
1, multi_signer.recov_hds[csv_values[1]], is_taproot
|
||||
)
|
||||
if is_taproot:
|
||||
all_xpubs = [
|
||||
hd
|
||||
for hd in multi_signer.prim_hds
|
||||
+ multi_signer.recov_hds[csv_values[0]]
|
||||
+ multi_signer.recov_hds[csv_values[1]]
|
||||
]
|
||||
internal_key = unspendable_internal_xpub(all_xpubs).get_xpub()
|
||||
# On purpose we use a single leaf instead of 3 different ones. It shouldn't be an issue.
|
||||
return f"tr([00000000]{internal_key}/<0;1>/*,or_d({prim_multi},or_i(and_v(v:{first_recov_multi},older({csv_values[0]})),and_v(v:{second_recov_multi},older({csv_values[1]})))))"
|
||||
else:
|
||||
return f"wsh(or_d({prim_multi},or_i(and_v(v:{first_recov_multi},older({csv_values[0]})),and_v(v:{second_recov_multi},older({csv_values[1]})))))"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lianad_multipath(bitcoind, directory):
|
||||
datadir = os.path.join(directory, "lianad")
|
||||
@ -211,12 +268,11 @@ def lianad_multipath(bitcoind, directory):
|
||||
|
||||
# A 3-of-4 that degrades into a 3-of-5 after 10 blocks and into a 1-of-10 after 20 blocks.
|
||||
csv_values = [10, 20]
|
||||
signer = MultiSigner(4, {csv_values[0]: 5, csv_values[1]: 10})
|
||||
prim_multi = multi_expression(3, signer.prim_hds)
|
||||
first_recov_multi = multi_expression(3, signer.recov_hds[csv_values[0]])
|
||||
second_recov_multi = multi_expression(1, signer.recov_hds[csv_values[1]])
|
||||
signer = MultiSigner(
|
||||
4, {csv_values[0]: 5, csv_values[1]: 10}, is_taproot=USE_TAPROOT
|
||||
)
|
||||
main_desc = Descriptor.from_str(
|
||||
f"wsh(or_d({prim_multi},or_i(and_v(v:{first_recov_multi},older({csv_values[0]})),and_v(v:{second_recov_multi},older({csv_values[1]})))))"
|
||||
multipath_desc(signer, csv_values, is_taproot=USE_TAPROOT)
|
||||
)
|
||||
|
||||
lianad = Lianad(
|
||||
|
||||
@ -4,4 +4,4 @@ pytest-timeout==1.3.4
|
||||
ephemeral_port_reserve==1.1.1
|
||||
|
||||
bip32~=3.0
|
||||
https://github.com/darosior/python-bip380/archive/f25eb2add9a5d461e382635231a5f971652fc8e1.zip
|
||||
https://github.com/darosior/python-bip380/archive/fb61971d9128e663f110ea2734c1d023e7e0266b.zip
|
||||
|
||||
@ -10,6 +10,7 @@ from test_framework.utils import (
|
||||
COIN,
|
||||
sign_and_broadcast,
|
||||
sign_and_broadcast_psbt,
|
||||
USE_TAPROOT,
|
||||
)
|
||||
from test_framework.serializations import PSBT
|
||||
|
||||
@ -354,6 +355,9 @@ def test_rescan_and_recovery(lianad, bitcoind):
|
||||
sign_and_broadcast(lianad, bitcoind, reco_psbt, recovery=True)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
USE_TAPROOT, reason="Needs a finalizer implemented in the Python test framework."
|
||||
)
|
||||
def test_conflicting_unconfirmed_spend_txs(lianad, bitcoind):
|
||||
"""Test we'll update the spending txid of a coin if a conflicting spend enters our mempool."""
|
||||
# Get an (unconfirmed, on purpose) coin to be spent by 2 different txs.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from bip32 import BIP32
|
||||
from bip32.utils import coincurve
|
||||
@ -9,10 +10,16 @@ from test_framework.serializations import (
|
||||
PSBT_IN_BIP32_DERIVATION,
|
||||
PSBT_IN_WITNESS_SCRIPT,
|
||||
PSBT_IN_PARTIAL_SIG,
|
||||
PSBT_IN_TAP_KEY_SIG,
|
||||
PSBT_IN_TAP_SCRIPT_SIG,
|
||||
PSBT_IN_TAP_LEAF_SCRIPT,
|
||||
PSBT_IN_TAP_BIP32_DERIVATION,
|
||||
PSBT_IN_TAP_INTERNAL_KEY,
|
||||
PSBT_IN_TAP_MERKLE_ROOT,
|
||||
)
|
||||
|
||||
|
||||
def sign_psbt(psbt, hds):
|
||||
def sign_psbt_wsh(psbt, hds):
|
||||
"""Sign a transaction.
|
||||
|
||||
This will fill the 'partial_sigs' field of all inputs.
|
||||
@ -58,12 +65,47 @@ def sign_psbt(psbt, hds):
|
||||
return psbt
|
||||
|
||||
|
||||
def sign_psbt_taproot(psbt, hds):
|
||||
"""Sign a transaction.
|
||||
|
||||
This will fill the 'tap_script_sig' / 'tap_key_sig' field of all inputs.
|
||||
|
||||
:param psbt: PSBT of the transaction to be signed.
|
||||
:param hds: the BIP32 objects to sign the transaction with.
|
||||
:returns: PSBT with a signature in each input for the given keys.
|
||||
"""
|
||||
assert isinstance(psbt, PSBT)
|
||||
|
||||
# This file is under tests/test_framework/ and we want tests/tools/taproot_signer/target/release/taproot_signer.
|
||||
bin_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"tools",
|
||||
"taproot_signer",
|
||||
"target",
|
||||
"release",
|
||||
"taproot_signer",
|
||||
)
|
||||
if not os.path.exists(bin_path):
|
||||
raise Exception(
|
||||
"Please compile the Taproot signer under tests/tools using 'cargo bin --release'."
|
||||
)
|
||||
|
||||
psbt_str = psbt.to_base64()
|
||||
for hd in hds:
|
||||
xprv = hd.get_xpriv()
|
||||
proc = subprocess.run([bin_path, psbt_str, xprv], capture_output=True, check=True)
|
||||
psbt_str = proc.stdout.decode("utf-8")
|
||||
|
||||
return PSBT.from_base64(psbt_str)
|
||||
|
||||
|
||||
class SingleSigner:
|
||||
"""Assumes a simple 1-primary path 1-recovery path Liana descriptor."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, is_taproot):
|
||||
self.primary_hd = BIP32.from_seed(os.urandom(32), network="test")
|
||||
self.recovery_hd = BIP32.from_seed(os.urandom(32), network="test")
|
||||
self.is_taproot = is_taproot
|
||||
|
||||
def sign_psbt(self, psbt, recovery=False):
|
||||
"""Sign a transaction.
|
||||
@ -75,13 +117,17 @@ class SingleSigner:
|
||||
:returns: PSBT with a signature in each input for the specified key.
|
||||
"""
|
||||
assert isinstance(recovery, bool)
|
||||
return sign_psbt(psbt, [self.recovery_hd if recovery else self.primary_hd])
|
||||
if self.is_taproot:
|
||||
return sign_psbt_taproot(
|
||||
psbt, [self.recovery_hd if recovery else self.primary_hd]
|
||||
)
|
||||
return sign_psbt_wsh(psbt, [self.recovery_hd if recovery else self.primary_hd])
|
||||
|
||||
|
||||
class MultiSigner:
|
||||
"""A signer that has multiple keys and may have multiple recovery path."""
|
||||
|
||||
def __init__(self, primary_hds_count, recovery_hds_counts):
|
||||
def __init__(self, primary_hds_count, recovery_hds_counts, is_taproot):
|
||||
self.prim_hds = [
|
||||
BIP32.from_seed(os.urandom(32), network="test")
|
||||
for _ in range(primary_hds_count)
|
||||
@ -91,6 +137,7 @@ class MultiSigner:
|
||||
self.recov_hds[timelock] = [
|
||||
BIP32.from_seed(os.urandom(32), network="test") for _ in range(count)
|
||||
]
|
||||
self.is_taproot = is_taproot
|
||||
|
||||
def sign_psbt(self, psbt, key_indices):
|
||||
"""Sign a transaction with the keys at the specified indices.
|
||||
@ -106,4 +153,6 @@ class MultiSigner:
|
||||
]
|
||||
else:
|
||||
hds = [self.prim_hds[i] for i in key_indices]
|
||||
return sign_psbt(psbt, hds)
|
||||
if self.is_taproot:
|
||||
return sign_psbt_taproot(psbt, hds)
|
||||
return sign_psbt_wsh(psbt, hds)
|
||||
|
||||
@ -24,6 +24,7 @@ DEFAULT_BITCOIND_PATH = "bitcoind"
|
||||
BITCOIND_PATH = os.getenv("BITCOIND_PATH", DEFAULT_BITCOIND_PATH)
|
||||
OLD_LIANAD_PATH = os.getenv("OLD_LIANAD_PATH", None)
|
||||
IS_NOT_BITCOIND_24 = bool(int(os.getenv("IS_NOT_BITCOIND_24", True)))
|
||||
USE_TAPROOT = bool(int(os.getenv("USE_TAPROOT", False))) # TODO: switch to True in a couple releases.
|
||||
|
||||
|
||||
COIN = 10**8
|
||||
@ -71,6 +72,21 @@ def get_txid(hex_tx):
|
||||
return tx.txid().hex()
|
||||
|
||||
|
||||
def sign_and_broadcast(lianad, bitcoind, psbt, recovery=False):
|
||||
"""Sign a PSBT, finalize it, extract the transaction and broadcast it."""
|
||||
signed_psbt = lianad.signer.sign_psbt(psbt, recovery)
|
||||
# Under Taproot i didn't bother implementing a finalizer in the test suite.
|
||||
if USE_TAPROOT:
|
||||
lianad.rpc.updatespend(signed_psbt.to_base64())
|
||||
txid = signed_psbt.tx.txid().hex()
|
||||
lianad.rpc.broadcastspend(txid)
|
||||
lianad.rpc.delspendtx(txid)
|
||||
return txid
|
||||
finalized_psbt = lianad.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
return bitcoind.rpc.sendrawtransaction(tx)
|
||||
|
||||
|
||||
def spend_coins(lianad, bitcoind, coins):
|
||||
"""Spend these coins, no matter how.
|
||||
This will create a single transaction spending them all at once at the minimum
|
||||
@ -84,21 +100,8 @@ def spend_coins(lianad, bitcoind, coins):
|
||||
bitcoind.rpc.getnewaddress(): total_value - 11 - 31 - 300 * len(coins)
|
||||
}
|
||||
res = lianad.rpc.createspend(destinations, [c["outpoint"] for c in coins], 1)
|
||||
|
||||
signed_psbt = lianad.signer.sign_psbt(PSBT.from_base64(res["psbt"]))
|
||||
finalized_psbt = lianad.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
bitcoind.rpc.sendrawtransaction(tx)
|
||||
|
||||
return tx
|
||||
|
||||
|
||||
def sign_and_broadcast(lianad, bitcoind, psbt, recovery=False):
|
||||
"""Sign a PSBT, finalize it, extract the transaction and broadcast it."""
|
||||
signed_psbt = lianad.signer.sign_psbt(psbt, recovery)
|
||||
finalized_psbt = lianad.finalize_psbt(signed_psbt)
|
||||
tx = finalized_psbt.tx.serialize_with_witness().hex()
|
||||
return bitcoind.rpc.sendrawtransaction(tx)
|
||||
txid = sign_and_broadcast(lianad, bitcoind, PSBT.from_base64(res["psbt"]))
|
||||
return bitcoind.rpc.getrawtransaction(txid)
|
||||
|
||||
|
||||
def sign_and_broadcast_psbt(lianad, psbt):
|
||||
|
||||
@ -14,6 +14,7 @@ from test_framework.utils import (
|
||||
COIN,
|
||||
TIMEOUT,
|
||||
IS_NOT_BITCOIND_24,
|
||||
USE_TAPROOT,
|
||||
)
|
||||
|
||||
from threading import Thread
|
||||
@ -66,9 +67,7 @@ def receive_and_send(lianad, bitcoind):
|
||||
psbt = PSBT.from_base64(res["psbt"])
|
||||
txid = psbt.tx.txid().hex()
|
||||
# If we sign only with two keys it won't be able to finalize
|
||||
with pytest.raises(
|
||||
RpcError, match="Miniscript Error: could not satisfy at index 0"
|
||||
):
|
||||
with pytest.raises(RpcError, match="ould not satisfy.* at index 0"):
|
||||
signed_psbt = lianad.signer.sign_psbt(psbt, range(2))
|
||||
lianad.rpc.updatespend(signed_psbt.to_base64())
|
||||
lianad.rpc.broadcastspend(txid)
|
||||
@ -242,7 +241,7 @@ def test_coinbase_deposit(lianad, bitcoind):
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
OLD_LIANAD_PATH is None, reason="Need the old lianad binary to create the datadir."
|
||||
OLD_LIANAD_PATH is None or USE_TAPROOT, reason="Need the old lianad binary to create the datadir."
|
||||
)
|
||||
def test_migration(lianad_multisig, bitcoind):
|
||||
"""Test we can start a newer lianad on a datadir created by an older lianad."""
|
||||
|
||||
@ -17,6 +17,7 @@ from test_framework.utils import (
|
||||
spend_coins,
|
||||
sign_and_broadcast,
|
||||
sign_and_broadcast_psbt,
|
||||
USE_TAPROOT,
|
||||
)
|
||||
|
||||
|
||||
@ -405,13 +406,21 @@ def test_create_spend(lianad, bitcoind):
|
||||
assert len(spend_psbt.o) == 4
|
||||
assert len(spend_psbt.tx.vout) == 4
|
||||
|
||||
# The transaction must contain the spent transaction for each input.
|
||||
# The transaction must contain the spent transaction for each input for P2WSH. But not for Taproot.
|
||||
# We don't make assumptions about the ordering of PSBT inputs.
|
||||
assert sorted(
|
||||
[psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i]
|
||||
) == sorted(
|
||||
[bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"]) for op in outpoints]
|
||||
)
|
||||
if USE_TAPROOT:
|
||||
assert all(
|
||||
PSBT_IN_NON_WITNESS_UTXO not in psbt_in.map for psbt_in in spend_psbt.i
|
||||
)
|
||||
else:
|
||||
assert sorted(
|
||||
[psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in spend_psbt.i]
|
||||
) == sorted(
|
||||
[
|
||||
bytes.fromhex(bitcoind.rpc.gettransaction(op[:64])["hex"])
|
||||
for op in outpoints
|
||||
]
|
||||
)
|
||||
|
||||
# We can sign it and broadcast it.
|
||||
sign_and_broadcast(lianad, bitcoind, PSBT.from_base64(res["psbt"]))
|
||||
@ -1092,9 +1101,9 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
|
||||
rbf_1_res = lianad.rpc.rbfpsbt(first_txid, False, 10)
|
||||
rbf_1_psbt = PSBT.from_base64(rbf_1_res["psbt"])
|
||||
# The inputs are the same in both (no new inputs needed in the replacement).
|
||||
assert sorted(
|
||||
psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in first_psbt.i
|
||||
) == sorted(psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_1_psbt.i)
|
||||
assert sorted(i.prevout.serialize() for i in first_psbt.tx.vin) == sorted(
|
||||
i.prevout.serialize() for i in rbf_1_psbt.tx.vin
|
||||
)
|
||||
# Check non-change output is the same in both.
|
||||
assert first_psbt.tx.vout[0].nValue == rbf_1_psbt.tx.vout[0].nValue
|
||||
assert first_psbt.tx.vout[0].scriptPubKey == rbf_1_psbt.tx.vout[0].scriptPubKey
|
||||
@ -1114,8 +1123,9 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
|
||||
# transaction:
|
||||
with pytest.raises(RpcError, match=f"Feerate too low: 10."):
|
||||
lianad.rpc.rbfpsbt(first_txid, False, 10)
|
||||
# Using 11 for feerate works.
|
||||
lianad.rpc.rbfpsbt(first_txid, False, 11)
|
||||
# Using 11 for feerate works for P2WSH. For Taproot we need 12.
|
||||
feerate = 12 if USE_TAPROOT else 11
|
||||
lianad.rpc.rbfpsbt(first_txid, False, feerate)
|
||||
# Add a new transaction spending the change from the first RBF.
|
||||
desc_1_destinations = {
|
||||
bitcoind.rpc.getnewaddress(): 500_000,
|
||||
@ -1149,12 +1159,12 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
|
||||
)
|
||||
)
|
||||
# Now replace the first RBF, which will also remove its descendants.
|
||||
rbf_2_res = lianad.rpc.rbfpsbt(rbf_1_txid, False, 11)
|
||||
rbf_2_res = lianad.rpc.rbfpsbt(rbf_1_txid, False, feerate)
|
||||
rbf_2_psbt = PSBT.from_base64(rbf_2_res["psbt"])
|
||||
# The inputs are the same in both (no new inputs needed in the replacement).
|
||||
assert sorted(
|
||||
psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_1_psbt.i
|
||||
) == sorted(psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_2_psbt.i)
|
||||
assert sorted(i.prevout.serialize() for i in rbf_1_psbt.tx.vin) == sorted(
|
||||
i.prevout.serialize() for i in rbf_2_psbt.tx.vin
|
||||
)
|
||||
# Check non-change output is the same in both.
|
||||
assert rbf_1_psbt.tx.vout[0].nValue == rbf_2_psbt.tx.vout[0].nValue
|
||||
assert rbf_1_psbt.tx.vout[0].scriptPubKey == rbf_2_psbt.tx.vout[0].scriptPubKey
|
||||
@ -1204,7 +1214,8 @@ def test_rbfpsbt_insufficient_funds(lianad, bitcoind):
|
||||
spend_txid_1 = sign_and_broadcast_psbt(lianad, spend_psbt_1)
|
||||
|
||||
# We don't have sufficient funds to bump the fee.
|
||||
assert "missing" in lianad.rpc.rbfpsbt(spend_txid_1, False, 2)
|
||||
feerate = 3 if USE_TAPROOT else 2
|
||||
assert "missing" in lianad.rpc.rbfpsbt(spend_txid_1, False, feerate)
|
||||
# We can still cancel it as the coin has enough value to create a single
|
||||
# output at a higher feerate.
|
||||
assert "psbt" in lianad.rpc.rbfpsbt(spend_txid_1, True)
|
||||
@ -1273,8 +1284,8 @@ def test_rbfpsbt_cancel(lianad, bitcoind):
|
||||
# Replacement only has a single input.
|
||||
assert len(rbf_1_psbt.i) == 1
|
||||
# This input is one of the two from the previous transaction.
|
||||
assert rbf_1_psbt.i[0].map[PSBT_IN_NON_WITNESS_UTXO] in [
|
||||
psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] for psbt_in in rbf_1_psbt.i
|
||||
assert rbf_1_psbt.tx.vin[0].prevout.serialize() in [
|
||||
i.prevout.serialize() for i in first_psbt.tx.vin
|
||||
]
|
||||
# The replacement only has a change output.
|
||||
assert len(rbf_1_psbt.tx.vout) == 1
|
||||
@ -1340,13 +1351,12 @@ def test_rbfpsbt_cancel(lianad, bitcoind):
|
||||
# Now cancel the first RBF, which will also remove its descendants.
|
||||
rbf_2_res = lianad.rpc.rbfpsbt(rbf_1_txid, True)
|
||||
rbf_2_psbt = PSBT.from_base64(rbf_2_res["psbt"])
|
||||
#
|
||||
assert len(rbf_2_psbt.i) == 1
|
||||
assert (
|
||||
rbf_1_psbt.i[0].map[PSBT_IN_NON_WITNESS_UTXO]
|
||||
== rbf_2_psbt.i[0].map[PSBT_IN_NON_WITNESS_UTXO]
|
||||
)
|
||||
# The inputs are the same in both (no new inputs needed in the replacement).
|
||||
assert len(rbf_2_psbt.tx.vin) == 1
|
||||
assert (
|
||||
rbf_1_psbt.tx.vin[0].prevout.serialize()
|
||||
== rbf_2_psbt.tx.vin[0].prevout.serialize()
|
||||
)
|
||||
|
||||
# Only a single output (change) in the replacement.
|
||||
assert len(rbf_2_psbt.tx.vout) == 1
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from fixtures import *
|
||||
from test_framework.serializations import PSBT, uint256_from_str
|
||||
from test_framework.utils import sign_and_broadcast_psbt, wait_for, COIN, RpcError
|
||||
from test_framework.utils import sign_and_broadcast_psbt, wait_for, COIN, RpcError, USE_TAPROOT
|
||||
|
||||
|
||||
def test_spend_change(lianad, bitcoind):
|
||||
@ -121,11 +121,12 @@ def test_coin_marked_spent(lianad, bitcoind):
|
||||
res = lianad.rpc.createspend(destinations, [outpoint], 1)
|
||||
psbt = PSBT.from_base64(res["psbt"])
|
||||
sign_and_broadcast(psbt)
|
||||
change_amount = 840 if USE_TAPROOT else 830
|
||||
assert len(psbt.o) == 1
|
||||
assert len(res["warnings"]) == 1
|
||||
assert (
|
||||
res["warnings"][0]
|
||||
== "Change amount of 830 sats added to fee as it was too small to create a transaction output."
|
||||
== f"Change amount of {change_amount} sats added to fee as it was too small to create a transaction output."
|
||||
)
|
||||
|
||||
# Spend the third coin to an address of ours, no change
|
||||
@ -137,11 +138,12 @@ def test_coin_marked_spent(lianad, bitcoind):
|
||||
res = lianad.rpc.createspend(destinations, [outpoint_3], 1)
|
||||
psbt = PSBT.from_base64(res["psbt"])
|
||||
sign_and_broadcast(psbt)
|
||||
change_amount = 828 if USE_TAPROOT else 818
|
||||
assert len(psbt.o) == 1
|
||||
assert len(res["warnings"]) == 1
|
||||
assert (
|
||||
res["warnings"][0]
|
||||
== "Change amount of 818 sats added to fee as it was too small to create a transaction output."
|
||||
== f"Change amount of {change_amount} sats added to fee as it was too small to create a transaction output."
|
||||
)
|
||||
|
||||
# Spend the fourth coin to an address of ours, with change
|
||||
@ -223,7 +225,8 @@ def test_send_to_self(lianad, bitcoind):
|
||||
assert len(spend_psbt.o) == len(spend_psbt.tx.vout) == 1
|
||||
|
||||
# Note they may ask for an impossible send-to-self. In this case we'll report missing amount.
|
||||
assert "missing" in lianad.rpc.createspend({}, outpoints, 40500)
|
||||
huge_feerate = 50_000 if USE_TAPROOT else 40_500
|
||||
assert "missing" in lianad.rpc.createspend({}, outpoints, huge_feerate)
|
||||
|
||||
# Sign and broadcast the send-to-self transaction created above.
|
||||
signed_psbt = lianad.signer.sign_psbt(spend_psbt)
|
||||
@ -237,7 +240,12 @@ def test_send_to_self(lianad, bitcoind):
|
||||
# FIXME: a 15% increase is huge.
|
||||
res = bitcoind.rpc.getmempoolentry(spend_txid)
|
||||
spend_feerate = int(res["fees"]["base"] * COIN / res["vsize"])
|
||||
assert specified_feerate <= spend_feerate <= int(specified_feerate * 115 / 100)
|
||||
if not USE_TAPROOT:
|
||||
assert specified_feerate <= spend_feerate <= int(specified_feerate * 115 / 100)
|
||||
else:
|
||||
# FIXME: under Taproot we should not consider the max feerate of all leaves if there
|
||||
# is a spendable internal key.
|
||||
assert specified_feerate <= spend_feerate <= int(specified_feerate * 125 / 100)
|
||||
|
||||
# We should by now only have one coin.
|
||||
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
||||
|
||||
90
tests/tools/taproot_signer/Cargo.lock
generated
Normal file
90
tests/tools/taproot_signer/Cargo.lock
generated
Normal file
@ -0,0 +1,90 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.10.0-beta"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bech32",
|
||||
"bitcoin-internals",
|
||||
"bitcoin_hashes",
|
||||
"hex-conservative",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-internals"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
||||
dependencies = [
|
||||
"bitcoin-internals",
|
||||
"hex-conservative",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
|
||||
|
||||
[[package]]
|
||||
name = "hex_lit"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"secp256k1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taproot_signer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
]
|
||||
10
tests/tools/taproot_signer/Cargo.toml
Normal file
10
tests/tools/taproot_signer/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "taproot_signer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.31", features = ["base64"] }
|
||||
|
||||
# Avoid it being built under Liana's target dir.
|
||||
[workspace]
|
||||
13
tests/tools/taproot_signer/README.md
Normal file
13
tests/tools/taproot_signer/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
A quick and dirty program to sign Taproot PSBTs with a given xpriv. This reuses the hot signer Rust
|
||||
code so i don't have to reimplement everything in Python.
|
||||
|
||||
Usage:
|
||||
```
|
||||
cargo build --release
|
||||
./target/release/taproot_signer "cHNidP8BAFICAAAAASLJVdEybEXc7wUWShMaYhEOjwJL5MYbzg+7zeR44BNGEwAAAAD9////AfRHAAAAAAAAFgAUZb+ZwCt1P282vnthp4cBgN4YNCgAAAAAAAEBKzhKAAAAAAAAIlEggmdBglq+Jt7mG8eFAZJPZ9TNEoqsfUXX+e0tRR4on0pCFcHpCSnxiCBSU6Ddvmwxv9nF+529tCGtEuCM1/zRYlksP2wAEs3HghglLpv2X3mSwNNzlaHY8BKNNas/13WibyuQaSCNnIknSSMfLln9wxqrj+HdoimEWq8EF48QCVphtE8of6wgQC+hw/bp6Z3LosvoFKmVGbi5BhTFxhJ88YlUkHwkyx26IFRPnR3hiTo2F1MW+PzS6hKLjdiQA+aD3DlMZkolBdKAulKcwEIVwekJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08ntsmIEkh6Q/Tvcb69Lv4z1KGhY5rH2SVgNb+0L6jjg9jTCA7rQEussAhFkAvocP26emdy6LL6BSplRm4uQYUxcYSfPGJVJB8JMsdOQHYcUYDJA2owfPHQJ6M6BcHx09671ybjswQqoJGvTye29gS2ksSAACAGAAAgB8AAAAAAAAAKgAAACEWSSHpD9O9xvr0u/jPUoaFjmsfZJWA1v7QvqOOD2NMIDtFAWwAEs3HghglLpv2X3mSwNNzlaHY8BKNNas/13WibyuQsLhRtwEAAAACAACAAwAAAAQAAIAFAAAABgAAAAAAAAAqAAAAIRZUT50d4Yk6NhdTFvj80uoSi43YkAPmg9w5TGZKJQXSgDUB2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08ntuwuFG3EgAAgBkAAIAAAAAAKgAAACEWjZyJJ0kjHy5Z/cMaq4/h3aIphFqvBBePEAlaYbRPKH9BAdhxRgMkDajB88dAnozoFwfHT3rvXJuOzBCqgka9PJ7bJxAoeAAAAIAMAACAKgAAAKQBAAA4AAAAAAAAACoAAAAhFukJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/DQB8Rh5dAAAAACoAAAABFyDpCSnxiCBSU6Ddvmwxv9nF+529tCGtEuCM1/zRYlksPwEYIKKzzcTwLrwB4DXcLXJYeZVMlpkb1zTIpFzsTWdZJU7nAAA=" xprv9s21ZrQH143K3fRVbj9JuCc3Eaph4F7AKXUtLGs9AWxWmhyMGq5ur5NqH3fDuiQZ6GamXbW7L3msaCwRbstxVqc3eP2FX3oa9wABxVzHr1k
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
cHNidP8BAFICAAAAASLJVdEybEXc7wUWShMaYhEOjwJL5MYbzg+7zeR44BNGEwAAAAD9////AfRHAAAAAAAAFgAUZb+ZwCt1P282vnthp4cBgN4YNCgAAAAAAAEBKzhKAAAAAAAAIlEggmdBglq+Jt7mG8eFAZJPZ9TNEoqsfUXX+e0tRR4on0pBFI2ciSdJIx8uWf3DGquP4d2iKYRarwQXjxAJWmG0Tyh/2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08nttAJ148PhMM/k/GnKCQ09RkpEKYCAHwVh3UMueWbq0TcIMW8s3XKNk1vyFy3LrXdnGghS8Up/i4bdNR2ikX1XdlYEIVwekJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/bAASzceCGCUum/ZfeZLA03OVodjwEo01qz/XdaJvK5BpII2ciSdJIx8uWf3DGquP4d2iKYRarwQXjxAJWmG0Tyh/rCBAL6HD9unpncuiy+gUqZUZuLkGFMXGEnzxiVSQfCTLHbogVE+dHeGJOjYXUxb4/NLqEouN2JAD5oPcOUxmSiUF0oC6UpzAQhXB6Qkp8YggUlOg3b5sMb/ZxfudvbQhrRLgjNf80WJZLD/YcUYDJA2owfPHQJ6M6BcHx09671ybjswQqoJGvTye2yYgSSHpD9O9xvr0u/jPUoaFjmsfZJWA1v7QvqOOD2NMIDutAS6ywCEWQC+hw/bp6Z3LosvoFKmVGbi5BhTFxhJ88YlUkHwkyx05AdhxRgMkDajB88dAnozoFwfHT3rvXJuOzBCqgka9PJ7b2BLaSxIAAIAYAACAHwAAAAAAAAAqAAAAIRZJIekP073G+vS7+M9ShoWOax9klYDW/tC+o44PY0wgO0UBbAASzceCGCUum/ZfeZLA03OVodjwEo01qz/XdaJvK5CwuFG3AQAAAAIAAIADAAAABAAAgAUAAAAGAAAAAAAAACoAAAAhFlRPnR3hiTo2F1MW+PzS6hKLjdiQA+aD3DlMZkolBdKANQHYcUYDJA2owfPHQJ6M6BcHx09671ybjswQqoJGvTye27C4UbcSAACAGQAAgAAAAAAqAAAAIRaNnIknSSMfLln9wxqrj+HdoimEWq8EF48QCVphtE8of0EB2HFGAyQNqMHzx0CejOgXB8dPeu9cm47MEKqCRr08ntsnECh4AAAAgAwAAIAqAAAApAEAADgAAAAAAAAAKgAAACEW6Qkp8YggUlOg3b5sMb/ZxfudvbQhrRLgjNf80WJZLD8NAHxGHl0AAAAAKgAAAAEXIOkJKfGIIFJToN2+bDG/2cX7nb20Ia0S4IzX/NFiWSw/ARggorPNxPAuvAHgNdwtclh5lUyWmRvXNMikXOxNZ1klTucAAA==
|
||||
```
|
||||
119
tests/tools/taproot_signer/src/main.rs
Normal file
119
tests/tools/taproot_signer/src/main.rs
Normal file
@ -0,0 +1,119 @@
|
||||
//! A quick and dirty program which reads a PSBT and an xpriv from stdin and outputs the signed
|
||||
//! PSBT to stdout. Uses function copied from Liana's hot signer and adapted.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
io::{self, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitcoin::{
|
||||
self,
|
||||
bip32::{self, Xpriv},
|
||||
hashes::Hash,
|
||||
key::TapTweak,
|
||||
psbt::{Input as PsbtIn, Psbt},
|
||||
secp256k1, sighash,
|
||||
};
|
||||
|
||||
fn sign_taproot(
|
||||
secp: &secp256k1::Secp256k1<secp256k1::All>,
|
||||
sighash_cache: &mut sighash::SighashCache<&bitcoin::Transaction>,
|
||||
master_xpriv: Xpriv,
|
||||
master_fingerprint: bip32::Fingerprint,
|
||||
prevouts: &[bitcoin::TxOut],
|
||||
psbt_in: &mut PsbtIn,
|
||||
input_index: usize,
|
||||
) {
|
||||
let sig_type = sighash::TapSighashType::Default;
|
||||
let prevouts = sighash::Prevouts::All(prevouts);
|
||||
|
||||
// If the details of the internal key are filled, provide a keypath signature.
|
||||
if let Some(ref int_key) = psbt_in.tap_internal_key {
|
||||
// NB: we don't check for empty leaf hashes on purpose, in case the internal key also
|
||||
// appears in a leaf.
|
||||
if let Some((_, (fg, der_path))) = psbt_in.tap_key_origins.get(int_key) {
|
||||
if *fg == master_fingerprint {
|
||||
let privkey = master_xpriv.derive_priv(secp, der_path).unwrap().to_priv();
|
||||
let keypair = secp256k1::Keypair::from_secret_key(secp, &privkey.inner);
|
||||
assert_eq!(keypair.x_only_public_key().0, *int_key);
|
||||
let keypair = keypair.tap_tweak(secp, psbt_in.tap_merkle_root).to_inner();
|
||||
let sighash = sighash_cache
|
||||
.taproot_key_spend_signature_hash(input_index, &prevouts, sig_type)
|
||||
.unwrap();
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&sighash, &keypair);
|
||||
let sig = bitcoin::taproot::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
};
|
||||
psbt_in.tap_key_sig = Some(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now sign for all the public keys derived from our master secret, in all the leaves where
|
||||
// they are present.
|
||||
for (pubkey, (leaf_hashes, (fg, der_path))) in &psbt_in.tap_key_origins {
|
||||
if *fg != master_fingerprint {
|
||||
continue;
|
||||
}
|
||||
|
||||
for leaf_hash in leaf_hashes {
|
||||
let privkey = master_xpriv.derive_priv(secp, der_path).unwrap().to_priv();
|
||||
let keypair = secp256k1::Keypair::from_secret_key(secp, &privkey.inner);
|
||||
let sighash = sighash_cache
|
||||
.taproot_script_spend_signature_hash(input_index, &prevouts, *leaf_hash, sig_type)
|
||||
.unwrap();
|
||||
let sighash = secp256k1::Message::from_digest_slice(sighash.as_byte_array())
|
||||
.expect("Sighash is always 32 bytes.");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&sighash, &keypair);
|
||||
let sig = bitcoin::taproot::Signature {
|
||||
sig,
|
||||
hash_ty: sig_type,
|
||||
};
|
||||
psbt_in.tap_script_sigs.insert((*pubkey, *leaf_hash), sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_psbt(psbt: &mut Psbt, master_xpriv: Xpriv, secp: &secp256k1::Secp256k1<secp256k1::All>) {
|
||||
let mut sighash_cache = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
let master_fingerprint = master_xpriv.fingerprint(secp);
|
||||
|
||||
let prevouts: Vec<_> = psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|psbt_in| psbt_in.witness_utxo.clone())
|
||||
.collect();
|
||||
assert_eq!(prevouts.len(), psbt.inputs.len());
|
||||
|
||||
// Sign each input in the PSBT.
|
||||
for i in 0..psbt.inputs.len() {
|
||||
sign_taproot(
|
||||
secp,
|
||||
&mut sighash_cache,
|
||||
master_xpriv,
|
||||
master_fingerprint,
|
||||
&prevouts,
|
||||
&mut psbt.inputs[i],
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
assert_eq!(args.len(), 3);
|
||||
|
||||
let mut psbt = Psbt::from_str(&args[1]).unwrap();
|
||||
let xprv = Xpriv::from_str(&args[2]).unwrap();
|
||||
|
||||
let secp = secp256k1::Secp256k1::new();
|
||||
sign_psbt(&mut psbt, xprv, &secp);
|
||||
|
||||
let psbt_str = psbt.to_string();
|
||||
print!("{}", psbt_str);
|
||||
io::stdout().flush().unwrap();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user