Skip to content

Commit aba2afe

Browse files
kdmukaimoneymanolisstepansnigirev
authored
Feature: Add taproot psbt fields (#1837)
* Adds BIP-371 Taproot bip32 derivation fields Co-authored-by: moneymanolis <[email protected]> Co-authored-by: Stepan Snigirev <[email protected]>
1 parent a739a06 commit aba2afe

File tree

8 files changed

+140
-48
lines changed

8 files changed

+140
-48
lines changed

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ requests==2.26.0
1515
pysocks==1.7.1
1616
six==1.16.0
1717
stem==1.8.0
18-
embit==0.5.0
18+
embit==0.6.1
1919
psutil==5.9.0
2020
pyopenssl==20.0.1
2121
flask_wtf==0.15.1

requirements.txt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ ecdsa==0.18.0 \
163163
# via
164164
# bitbox02
165165
# hwi
166-
embit==0.5.0 \
167-
--hash=sha256:5644ae6ed07bb71bf7fb15daf7f5af73d889180e623f5ff1f35a20ad01f0405e
166+
embit==0.6.1 \
167+
--hash=sha256:16a84c6668dc9ffc907594457a46f7142cee379646bc009a5a9b77b0d2cb4e12
168168
# via -r requirements.in
169169
flask==2.1.1 \
170170
--hash=sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264 \
@@ -437,9 +437,9 @@ pytimeparse==1.1.8 \
437437
--hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \
438438
--hash=sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a
439439
# via -r requirements.in
440-
pytz==2022.4 \
441-
--hash=sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91 \
442-
--hash=sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174
440+
pytz==2022.5 \
441+
--hash=sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22 \
442+
--hash=sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914
443443
# via
444444
# apscheduler
445445
# babel
@@ -485,9 +485,9 @@ typing-extensions==3.10.0.2 \
485485
# via
486486
# bitbox02
487487
# hwi
488-
tzdata==2022.4 \
489-
--hash=sha256:74da81ecf2b3887c94e53fc1d466d4362aaf8b26fc87cda18f22004544694583 \
490-
--hash=sha256:ada9133fbd561e6ec3d1674d3fba50251636e918aa97bd59d63735bef5a513bb
488+
tzdata==2022.5 \
489+
--hash=sha256:323161b22b7802fdc78f20ca5f6073639c64f1a7227c40cd3e19fd1d0ce6650a \
490+
--hash=sha256:e15b2b3005e2546108af42a0eb4ccab4d9e225e2dfbf4f77aad50c70a4b1f3ab
491491
# via pytz-deprecation-shim
492492
tzlocal==4.2 \
493493
--hash=sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745 \
@@ -507,9 +507,9 @@ wtforms==3.0.1 \
507507
--hash=sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc \
508508
--hash=sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b
509509
# via flask-wtf
510-
zipp==3.8.1 \
511-
--hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \
512-
--hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009
510+
zipp==3.10.0 \
511+
--hash=sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1 \
512+
--hash=sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8
513513
# via importlib-metadata
514514

515515
# WARNING: The following packages were not pinned, but pip requires them to be

src/cryptoadvance/specter/device.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def has_key_types(self, wallet_type, network="main"):
159159
return True
160160
elif wallet_type == "simple":
161161
for key_type in self.key_types(network):
162-
if key_type in ["", "sh-wpkh", "wpkh"]:
162+
if key_type in ["", "sh-wpkh", "wpkh", "tr"]:
163163
return True
164164
return "" in self.key_types(network)
165165

src/cryptoadvance/specter/devices/specter.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,7 @@ def create_psbts(self, base64_psbt, wallet):
103103
base64_psbt = psbt.to_string()
104104
psbts = super().create_psbts(base64_psbt, wallet)
105105
# remove non-witness utxo if they are there to reduce QR code size
106-
updated_psbt = wallet.fill_psbt(
107-
base64_psbt, non_witness=False, xpubs=False, taproot_derivations=True
108-
)
106+
updated_psbt = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=False)
109107
try:
110108
qr_psbt = PSBT.from_string(updated_psbt)
111109
except:
@@ -141,12 +139,8 @@ def create_psbts(self, base64_psbt, wallet):
141139
psbts["qrcode"] = qr_psbt.to_string()
142140

143141
# we can add xpubs to SD card, but non_witness can be too large for MCU
144-
psbts["sdcard"] = wallet.fill_psbt(
145-
base64_psbt, non_witness=False, xpubs=True, taproot_derivations=True
146-
)
147-
psbts["hwi"] = wallet.fill_psbt(
148-
base64_psbt, non_witness=False, xpubs=True, taproot_derivations=True
149-
)
142+
psbts["sdcard"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True)
143+
psbts["hwi"] = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=True)
150144
return psbts
151145

152146
def export_wallet(self, wallet):

src/cryptoadvance/specter/util/psbt.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ def to_dict(self) -> dict:
160160
}
161161
for pub, der in self.scope.bip32_derivations.items()
162162
]
163+
if self.scope.taproot_bip32_derivations:
164+
obj["taproot_bip32_derivs"] = [
165+
{
166+
"pubkey": pub.xonly().hex(),
167+
"master_fingerprint": der.fingerprint.hex(),
168+
"path": bip32.path_to_str(der.derivation),
169+
"leaf_hashes": [leaf.hex() for leaf in leafs],
170+
}
171+
for pub, (leafs, der) in self.scope.taproot_bip32_derivations.items()
172+
]
173+
if self.scope.taproot_internal_key:
174+
obj["taproot_internal_key"] = self.scope.taproot_internal_key.xonly().hex()
163175
return obj
164176

165177

src/cryptoadvance/specter/wallet.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,8 +1642,9 @@ def createpsbt(
16421642
readonly=False, # fee estimation
16431643
rbf=True,
16441644
rbf_edit_mode=False,
1645-
):
1645+
) -> dict:
16461646
"""
1647+
Returns psbt as dictionary.
16471648
fee_rate: in sat/B or BTC/kB. If set to 0 Bitcoin Core sets feeRate automatically.
16481649
"""
16491650
if fee_rate != 0 and fee_rate < self.MIN_FEE_RATE:
@@ -1717,7 +1718,10 @@ def createpsbt(
17171718
True, # bip32-der
17181719
)
17191720

1720-
b64psbt = r["psbt"]
1721+
# Always explicitly fill psbt with any missing fields
1722+
# TODO: Re-evaluate if this is necessary if user is running Bitcoin Core w/BIP-371 support
1723+
b64psbt = self.fill_psbt(r["psbt"])
1724+
17211725
psbt = self.PSBTCls(
17221726
b64psbt,
17231727
self.descriptor,
@@ -1740,14 +1744,16 @@ def createpsbt(
17401744
True, # bip32-der
17411745
)
17421746

1743-
b64psbt = r["psbt"]
1747+
# Always explicitly fill psbt with any missing fields
1748+
# TODO: Re-evaluate if this is necessary if user is running Bitcoin Core w/BIP-371 support
1749+
b64psbt = self.fill_psbt(r["psbt"])
1750+
17441751
psbt = self.PSBTCls(
17451752
b64psbt,
17461753
self.descriptor,
17471754
self.network,
17481755
devices=list(zip(self.keys, self._devices)),
17491756
)
1750-
17511757
if not readonly:
17521758
self.save_pending_psbt(psbt)
17531759
return psbt.to_dict()
@@ -1866,25 +1872,36 @@ def fill_psbt(
18661872
b64psbt,
18671873
non_witness: bool = True,
18681874
xpubs: bool = True,
1869-
taproot_derivations: bool = False,
18701875
):
18711876
psbt = self.PSBTCls.from_string(b64psbt)
18721877

18731878
# Core doesn't fill derivations yet, so we do it ourselves
1874-
if taproot_derivations and self.is_taproot:
1875-
1879+
# Provide the BIP-371 `PSBT_IN_TAP_BIP32_DERIVATION` 0x16 field
1880+
if self.is_taproot:
18761881
net = self.network
18771882
for sc in psbt.inputs + psbt.outputs:
1883+
if sc.taproot_internal_key is not None:
1884+
# psbt already has Taproot fields for this `InputScope`/`OutputScope`
1885+
continue
18781886
addr = sc.script_pubkey.address(net)
18791887
info = self._addresses.get(addr)
18801888
if info and not info.is_external:
18811889
d = self.descriptor.derive(
18821890
info.index, branch_index=int(info.change)
18831891
)
18841892
for k in d.keys:
1885-
sc.bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
1893+
# TODO: support keysigns from within the taptree (note: embit
1894+
# must be updated first).
1895+
leaf_hashes = []
1896+
derivation = DerivationPath(
18861897
k.origin.fingerprint, k.origin.derivation
18871898
)
1899+
pub = PublicKey.from_xonly(k.xonly())
1900+
sc.taproot_bip32_derivations[pub] = (
1901+
leaf_hashes,
1902+
derivation,
1903+
)
1904+
sc.taproot_internal_key = pub
18881905

18891906
if non_witness:
18901907
for inp in psbt.inputs:

tests/test_util_psbt.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from cryptoadvance.specter.util.psbt import SpecterPSBT, Descriptor
2+
3+
PSBT = "cHNidP8BAH0CAAAAAZ3WUGbo+qq+uhku8ZGlccpVnEUe7DpXc2WT8eFBOY5NAAAAAAD9////AoCWmAAAAAAAFgAUknv5/4QLPO9YhpvFXh8Yd1A2uiloP10FAAAAACJRIBTYMq2X8Wb48epOTevfvMFFs1Rywret7quv2PDmQ27QAAAAAAABASsA4fUFAAAAACJRIGGkhZUFZ4wSdPRYh8+NH8rT4OPZ96DG+4g40XzsrkswIRbrOsC4g4bRFEv7o2eV6PjsmXRITNcywDkjoKE3IXZEcBkAc8XaClYAAIABAACAAAAAgAAAAAAAAAAAARcg6zrAuIOG0RRL+6Nnlej47Jl0SEzXMsA5I6ChNyF2RHAAAAEFIPF/TUgvecmr7Omn2RaD3/WuEWxZkvyKAVX8FtRQMndaIQfxf01IL3nJq+zpp9kWg9/1rhFsWZL8igFV/BbUUDJ3WhkAc8XaClYAAIABAACAAAAAgAEAAAABAAAAAA=="
4+
DESC = "tr([73c5da0a/86h/1h/0h]tprv8h5RpVZ1VP6ZenvqJAuUYCenQqYgRjsAMjqnVY54FqQzk52jqP12mPHa77wXQm9WeJSRjDhT3N5RL2Ye93Z4kR6rWTNo25Tdq6UfopDczBZ/{0,1}/*)"
5+
6+
7+
def test_taproot_psbt_to_dict():
8+
psbt = SpecterPSBT(PSBT, Descriptor.from_string(DESC), "regtest")
9+
obj = psbt.to_dict()
10+
assert obj["inputs"][0]["taproot_bip32_derivs"] == [
11+
{
12+
"pubkey": "eb3ac0b88386d1144bfba36795e8f8ec9974484cd732c03923a0a13721764470",
13+
"master_fingerprint": "73c5da0a",
14+
"path": "m/86h/1h/0h/0/0",
15+
"leaf_hashes": [],
16+
}
17+
]
18+
assert (
19+
obj["inputs"][0]["taproot_internal_key"]
20+
== "eb3ac0b88386d1144bfba36795e8f8ec9974484cd732c03923a0a13721764470"
21+
)
22+
23+
assert obj["outputs"][1]["taproot_bip32_derivs"] == [
24+
{
25+
"pubkey": "f17f4d482f79c9abece9a7d91683dff5ae116c5992fc8a0155fc16d45032775a",
26+
"master_fingerprint": "73c5da0a",
27+
"path": "m/86h/1h/0h/1/1",
28+
"leaf_hashes": [],
29+
}
30+
]
31+
assert (
32+
obj["outputs"][1]["taproot_internal_key"]
33+
== "f17f4d482f79c9abece9a7d91683dff5ae116c5992fc8a0155fc16d45032775a"
34+
)
35+
assert obj["outputs"][1]["change"] == True
36+
assert obj["outputs"][1]["is_mine"] == True
37+
assert obj["inputs"][0]["is_mine"] == True

tests/test_wallet.py

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,46 +37,78 @@ def test_createpsbt(
3737
)
3838
unspents = wallet.rpc.listunspent()
3939
selected_coin = [{"txid": unspents[0]["txid"], "vout": unspents[0]["vout"]}]
40-
# Spending it all
4140
psbt = wallet.createpsbt(
4241
[random_address],
43-
[20],
44-
True,
42+
[19],
43+
False, # Important because otherwise there is no change output!
4544
0,
4645
1,
4746
selected_coins=selected_coin, # Selecting only one UTXO since input ordering seems to also be random in Core.
4847
)
4948
assert len(psbt["tx"]["vin"]) == 1
5049
assert len(psbt["inputs"]) == 1
5150

52-
# Check the PSBT fields - inputs
53-
# The first input seems to be last selected coin from selected_coins
51+
# Input fields
5452
assert (
5553
psbt["inputs"][0]["bip32_derivs"][0]["pubkey"]
5654
== "0330955ab511845fb48fc5739da551875ed54fa1f2fdd4cf77f3473ce2cffb4c75"
5755
)
5856
assert psbt["inputs"][0]["bip32_derivs"][0]["path"] == "m/84h/1h/0h/0/1"
5957
assert psbt["inputs"][0]["bip32_derivs"][0]["master_fingerprint"] == "8c24a510"
6058

61-
# Check the PSBT fields - outputs
62-
logger.info(f"the whole {psbt}")
63-
64-
logger.info(f"the outputs of the {psbt['outputs']}")
65-
assert (
66-
psbt["outputs"][0]["address"] == "bcrt1q7mlxxdna2e2ufzgalgp5zhtnndl7qddlxjy5eg"
67-
)
68-
assert psbt["outputs"][0]["change"] == False
69-
# assert psbt["outputs"][0]["bip32_derivs"][0]["master_fingerprint"] == "1e9cf8a7"
59+
# Output fields
60+
for output in psbt["outputs"]: # The ordering of the outputs is random
61+
if output["change"] == False:
62+
assert output["address"] == "bcrt1q7mlxxdna2e2ufzgalgp5zhtnndl7qddlxjy5eg"
63+
else:
64+
assert output["is_mine"] == True
65+
assert (
66+
output["bip32_derivs"][0]["pubkey"]
67+
== "02251fe2ee4bc43729b0903ffadbcf846d9e6acbb3aa593b09d60085645cbe3653"
68+
)
69+
assert output["bip32_derivs"][0]["path"] == "m/84h/1h/0h/1/0"
7070

71-
# Check the fields of a PSBT created by a taproot wallet
72-
# Could be moved to a dedicated test of taproot functionalites in the future
71+
# Taproot fields (could be moved to a dedicated test of taproot functionalites in the future)
7372
taproot_wallet = funded_taproot_wallet
7473
assert taproot_wallet.is_taproot == True
7574
address = taproot_wallet.getnewaddress()
76-
# Taproot test addrs are bcrt1p
7775
assert address.startswith("bcrt1p")
7876
assert taproot_wallet.amount_total == 20
79-
# TODO: Test psbts with taproot wallet, especially the new taproot fields.
77+
# Let's keep the random address so we have a "mixed" set of outputs: segwit and taproot
78+
psbt = taproot_wallet.createpsbt(
79+
[random_address],
80+
[3],
81+
False,
82+
0,
83+
1,
84+
)
85+
# Input fields
86+
assert psbt["inputs"][0]["taproot_bip32_derivs"][0]["path"] == "m/86h/1h/0h/0/1"
87+
assert (
88+
psbt["inputs"][0]["taproot_bip32_derivs"][0]["master_fingerprint"] == "8c24a510"
89+
)
90+
assert psbt["inputs"][0]["taproot_bip32_derivs"][0]["leaf_hashes"] == []
91+
complete_pubkey = (
92+
"0274fea50d7f2a69489c2d2a146e317e02f47ad032e81b35fe6059e066670a100e"
93+
)
94+
assert (
95+
psbt["inputs"][0]["taproot_bip32_derivs"][0]["pubkey"] == complete_pubkey[2:]
96+
) # The pubkey is "xonly", for details: https://embit.rocks/#/api/ec/public_key?id=xonly
97+
# Output fields
98+
for output in psbt["outputs"]:
99+
if output["change"] == False:
100+
assert output["address"] == "bcrt1q7mlxxdna2e2ufzgalgp5zhtnndl7qddlxjy5eg"
101+
else:
102+
assert output["taproot_bip32_derivs"][0]["path"] == "m/86h/1h/0h/1/0"
103+
assert (
104+
output["taproot_bip32_derivs"][0]["pubkey"]
105+
== "85b747f5ffc1a1ff951790771c86b24725e283afb2d7e5b8392858bc04f5d05c"
106+
)
107+
assert (
108+
output["taproot_bip32_derivs"][0]["pubkey"]
109+
== output["taproot_internal_key"]
110+
)
111+
assert output["taproot_bip32_derivs"][0]["leaf_hashes"] == []
80112

81113

82114
@pytest.mark.slow

0 commit comments

Comments
 (0)