Browse Source

add TaprootWallet

add_frost_channel_encryption
zebra-lucky 12 months ago
parent
commit
6d05c1039c
  1. 4
      conftest.py
  2. 5
      pyproject.toml
  3. 6
      scripts/joinmarket-qt.py
  4. 243
      src/jmbitcoin/scripteval.py
  5. 90
      src/jmbitcoin/secp256k1_transaction.py
  6. 4
      src/jmclient/__init__.py
  7. 69
      src/jmclient/blockchaininterface.py
  8. 6
      src/jmclient/configure.py
  9. 31
      src/jmclient/cryptoengine.py
  10. 49
      src/jmclient/descriptor.py
  11. 3
      src/jmclient/maker.py
  12. 10
      src/jmclient/support.py
  13. 36
      src/jmclient/taker.py
  14. 95
      src/jmclient/wallet.py
  15. 103
      src/jmclient/wallet_service.py
  16. 45
      src/jmclient/wallet_utils.py
  17. 10
      src/jmclient/yieldgenerator.py
  18. 5
      src/jmdaemon/orderbookwatch.py
  19. 4
      src/jmdaemon/protocol.py
  20. 22
      test/jmclient/test_wallet.py

4
conftest.py

@ -75,7 +75,7 @@ def pytest_addoption(parser: Any) -> None:
default='bitcoinrpc',
help="the RPC username for your test bitcoin instance (default=bitcoinrpc)")
parser.addoption("--nirc",
type="int",
type=int,
action="store",
default=1,
help="the number of local miniircd instances")
@ -136,7 +136,7 @@ def setup_regtest_bitcoind(pytestconfig):
bitcoin_path = pytestconfig.getoption("--btcroot")
bitcoind_path = os.path.join(bitcoin_path, "bitcoind")
bitcoincli_path = os.path.join(bitcoin_path, "bitcoin-cli")
start_cmd = f'{bitcoind_path} -regtest -daemon -conf={conf}'
start_cmd = f'{bitcoind_path} -regtest -daemon -txindex -conf={conf}'
stop_cmd = f'{bitcoincli_path} -regtest -rpcuser={rpcuser} -rpcpassword={rpcpassword} stop'
# determine bitcoind version

5
pyproject.toml

@ -34,6 +34,8 @@ jmdaemon = [
"libnacl==1.8.0",
"pyopenssl==24.0.0",
]
jmfrost = [
]
jmqtui = [
"PyQt5!=5.15.0,!=5.15.1,!=5.15.2,!=6.0",
"PySide2!=5.15.0,!=5.15.1,!=5.15.2,!=6.0", # https://bugreports.qt.io/browse/QTBUG-88688
@ -44,6 +46,7 @@ jmqtui = [
client = [
"joinmarket[jmclient]",
"joinmarket[jmbitcoin]",
"joinmarket[jmfrost]",
]
daemon = [
"joinmarket[jmdaemon]",
@ -76,4 +79,4 @@ where = ["src"]
exclude = ["*.test"]
[tool.pytest.ini_options]
testpaths = ["test"]
testpaths = ["test"]

6
scripts/joinmarket-qt.py

@ -992,10 +992,12 @@ class SpendTab(QWidget):
mbinfo.append("Counterparties chosen:")
mbinfo.append('Name, Order id, Coinjoin fee (sat.)')
for k, o in offers.items():
if o['ordertype'] in ['sw0reloffer', 'swreloffer', 'reloffer']:
if o['ordertype'] in ['trreloffer', 'sw0reloffer',
'swreloffer', 'reloffer']:
display_fee = int(self.taker.cjamount *
float(o['cjfee'])) - int(o['txfee'])
elif o['ordertype'] in ['sw0absoffer', 'swabsoffer', 'absoffer']:
elif o['ordertype'] in ['trabsoffer', 'sw0absoffer',
'swabsoffer', 'absoffer']:
display_fee = int(o['cjfee']) - int(o['txfee'])
else:
log.debug("Unsupported order type: " + str(o['ordertype']) +

243
src/jmbitcoin/scripteval.py

@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
import hashlib
from typing import List, Optional, Tuple, Union, Set, Type
import bitcointx
from bitcointx.core.key import XOnlyPubKey
from bitcointx.core.script import SIGVERSION_TAPROOT, SignatureHashSchnorr
from bitcointx.core import CTxOut
from bitcointx.core.scripteval import (
CScript, ScriptVerifyFlag_Type, CScriptWitness, VerifyScriptError,
STANDARD_SCRIPT_VERIFY_FLAGS, UNHANDLED_SCRIPT_VERIFY_FLAGS, EvalScript,
SCRIPT_VERIFY_CLEANSTACK, script_verify_flags_to_string, _CastToBool,
ensure_isinstance, SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM,
MAX_SCRIPT_ELEMENT_SIZE, OP_CHECKSIG, OP_EQUALVERIFY, OP_HASH160, OP_DUP)
from bitcointx.core.scripteval import (
SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_P2SH, SIGVERSION_WITNESS_V0)
def VerifyScriptWithTaproot(
scriptSig: CScript, scriptPubKey: CScript,
txTo: 'bitcointx.core.CTransaction', inIdx: int,
flags: Optional[Union[Tuple[ScriptVerifyFlag_Type, ...],
Set[ScriptVerifyFlag_Type]]] = None,
amount: int = 0, witness: Optional[CScriptWitness] = None,
*,
spent_outputs: Optional[List[CTxOut]] = None
) -> None:
"""Verify a scriptSig satisfies a scriptPubKey
scriptSig - Signature
scriptPubKey - PubKey
txTo - Spending transaction
inIdx - Index of the transaction input containing scriptSig
Raises a ValidationError subclass if the validation fails.
"""
ensure_isinstance(scriptSig, CScript, 'scriptSig')
if not type(scriptSig) == type(scriptPubKey): # noqa: exact class check
raise TypeError(
"scriptSig and scriptPubKey must be of the same script class")
script_class = scriptSig.__class__
if flags is None:
flags = STANDARD_SCRIPT_VERIFY_FLAGS - UNHANDLED_SCRIPT_VERIFY_FLAGS
else:
flags = set(flags) # might be passed as tuple
if flags & UNHANDLED_SCRIPT_VERIFY_FLAGS:
raise VerifyScriptError(
"some of the flags cannot be handled by current code: {}"
.format(script_verify_flags_to_string(flags & UNHANDLED_SCRIPT_VERIFY_FLAGS)))
stack: List[bytes] = []
EvalScript(stack, scriptSig, txTo, inIdx, flags=flags)
if SCRIPT_VERIFY_P2SH in flags:
stackCopy = list(stack)
EvalScript(stack, scriptPubKey, txTo, inIdx, flags=flags)
if len(stack) == 0:
raise VerifyScriptError("scriptPubKey left an empty stack")
if not _CastToBool(stack[-1]):
raise VerifyScriptError("scriptPubKey returned false")
hadWitness = False
if witness is None:
witness = CScriptWitness([])
if SCRIPT_VERIFY_WITNESS in flags and scriptPubKey.is_witness_scriptpubkey():
hadWitness = True
if scriptSig:
raise VerifyScriptError("scriptSig is not empty")
VerifyWitnessProgramWithTaproot(
witness,
scriptPubKey.witness_version(),
scriptPubKey.witness_program(),
txTo, inIdx, flags=flags, amount=amount,
script_class=script_class,
spent_outputs=spent_outputs)
# Bypass the cleanstack check at the end. The actual stack is obviously not clean
# for witness programs.
stack = stack[:1]
# Additional validation for spend-to-script-hash transactions
if SCRIPT_VERIFY_P2SH in flags and scriptPubKey.is_p2sh():
if not scriptSig.is_push_only():
raise VerifyScriptError("P2SH scriptSig not is_push_only()")
# restore stack
stack = stackCopy
# stack cannot be empty here, because if it was the
# P2SH HASH <> EQUAL scriptPubKey would be evaluated with
# an empty stack and the EvalScript above would return false.
assert len(stack)
pubKey2 = script_class(stack.pop())
EvalScript(stack, pubKey2, txTo, inIdx, flags=flags)
if not len(stack):
raise VerifyScriptError("P2SH inner scriptPubKey left an empty stack")
if not _CastToBool(stack[-1]):
raise VerifyScriptError("P2SH inner scriptPubKey returned false")
# P2SH witness program
if SCRIPT_VERIFY_WITNESS in flags and pubKey2.is_witness_scriptpubkey():
hadWitness = True
if scriptSig != script_class([pubKey2]):
raise VerifyScriptError("scriptSig is not exactly a single push of the redeemScript")
VerifyWitnessProgramWithTaproot(
witness,
pubKey2.witness_version(),
pubKey2.witness_program(),
txTo, inIdx, flags=flags, amount=amount,
script_class=script_class,
spent_outputs=spent_outputs)
# Bypass the cleanstack check at the end. The actual stack is obviously not clean
# for witness programs.
stack = stack[:1]
if SCRIPT_VERIFY_CLEANSTACK in flags:
if SCRIPT_VERIFY_P2SH not in flags:
raise ValueError(
'SCRIPT_VERIFY_CLEANSTACK requires SCRIPT_VERIFY_P2SH')
if len(stack) == 0:
raise VerifyScriptError("scriptPubKey left an empty stack")
elif len(stack) != 1:
raise VerifyScriptError("scriptPubKey left extra items on stack")
if SCRIPT_VERIFY_WITNESS in flags:
# We can't check for correct unexpected witness data if P2SH was off, so require
# that WITNESS implies P2SH. Otherwise, going from WITNESS->P2SH+WITNESS would be
# possible, which is not a softfork.
if SCRIPT_VERIFY_P2SH not in flags:
raise ValueError(
"SCRIPT_VERIFY_WITNESS requires SCRIPT_VERIFY_P2SH")
if not hadWitness and witness:
raise VerifyScriptError("Unexpected witness")
def VerifyWitnessProgramWithTaproot(
witness: CScriptWitness,
witversion: int, program: bytes,
txTo: 'bitcointx.core.CTransaction',
inIdx: int,
flags: Set[ScriptVerifyFlag_Type] = set(),
amount: int = 0,
script_class: Type[CScript] = CScript,
*,
spent_outputs: Optional[List[CTxOut]] = None
) -> None:
if script_class is None:
raise ValueError("script class must be specified")
sigversion = None
if witversion == 0:
sigversion = SIGVERSION_WITNESS_V0
stack = list(witness.stack)
if len(program) == 32:
# Version 0 segregated witness program: SHA256(CScript) inside the program,
# CScript + inputs in witness
if len(stack) == 0:
raise VerifyScriptError("witness is empty")
scriptPubKey = script_class(stack.pop())
hashScriptPubKey = hashlib.sha256(scriptPubKey).digest()
if hashScriptPubKey != program:
raise VerifyScriptError("witness program mismatch")
elif len(program) == 20:
# Special case for pay-to-pubkeyhash; signature + pubkey in witness
if len(stack) != 2:
raise VerifyScriptError("witness program mismatch") # 2 items in witness
scriptPubKey = script_class([OP_DUP, OP_HASH160, program,
OP_EQUALVERIFY, OP_CHECKSIG])
else:
raise VerifyScriptError("wrong length for witness program")
elif witversion == 1:
sigversion = SIGVERSION_TAPROOT
stack = list(witness.stack)
if len(program) == 32:
if len(stack) == 0:
raise VerifyScriptError("witness is empty")
if len(stack) != 1:
raise VerifyScriptError("only key path spend is supported")
assert spent_outputs
sig = stack[0]
pubkey = XOnlyPubKey(program)
sighash = SignatureHashSchnorr(txTo, inIdx, spent_outputs)
if pubkey.verify_schnorr(sighash, sig):
return
else:
raise VerifyScriptError("schnorr signature verify failed")
else:
raise VerifyScriptError("wrong length for witness program")
elif SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM in flags:
raise VerifyScriptError("upgradeable witness program is not accepted")
else:
# Higher version witness scripts return true for future softfork compatibility
return
assert sigversion is not None
for i, elt in enumerate(stack):
if isinstance(elt, int):
elt_len = len(script_class([elt]))
else:
elt_len = len(elt)
# Disallow stack item size > MAX_SCRIPT_ELEMENT_SIZE in witness stack
if elt_len > MAX_SCRIPT_ELEMENT_SIZE:
raise VerifyScriptError(
"maximum push size exceeded by an item at position {} "
"on witness stack".format(i))
EvalScript(stack, scriptPubKey, txTo, inIdx, flags=flags, amount=amount, sigversion=sigversion)
# Scripts inside witness implicitly require cleanstack behaviour
if len(stack) == 0:
raise VerifyScriptError("scriptPubKey left an empty stack")
elif len(stack) != 1:
raise VerifyScriptError("scriptPubKey left extra items on stack")
if not _CastToBool(stack[-1]):
raise VerifyScriptError("scriptPubKey returned false")
return

90
src/jmbitcoin/secp256k1_transaction.py

@ -12,15 +12,17 @@ from bitcointx.core import (CMutableTransaction, CTxInWitness,
CMutableTxOut, CTxIn, CTxOut, ValidationError,
CBitcoinTransaction)
from bitcointx.core.script import *
from bitcointx.wallet import (P2WPKHCoinAddress, CCoinAddress, P2PKHCoinAddress,
CCoinAddressError)
from bitcointx.core.scripteval import (VerifyScript, SCRIPT_VERIFY_WITNESS,
from bitcointx.core.script import SignatureHashSchnorr
from bitcointx.wallet import (P2WPKHCoinAddress, P2TRCoinAddress, CCoinAddress,
P2PKHCoinAddress, CCoinAddressError, CCoinKey)
from bitcointx.core.scripteval import (SCRIPT_VERIFY_WITNESS,
SCRIPT_VERIFY_P2SH,
SCRIPT_VERIFY_STRICTENC,
SIGVERSION_WITNESS_V0)
from jmbase import bintohex, utxo_to_utxostr
from jmbitcoin.secp256k1_main import *
from .scripteval import VerifyScriptWithTaproot as VerifyScript
def human_readable_transaction(tx: CTransaction, jsonified: bool = True) -> str:
@ -137,7 +139,8 @@ def estimate_tx_size(ins: List[str], outs: List[str]) -> Union[int, Tuple[int]]:
inmults = {"p2wsh": {"w": 1 + 72 + 43, "nw": 41},
"p2wpkh": {"w": 108, "nw": 41},
"p2sh-p2wpkh": {"w": 108, "nw": 64},
"p2pkh": {"w": 0, "nw": 148}}
"p2pkh": {"w": 0, "nw": 148},
"p2tr": {"w": 67, "nw": 41}}
# Notes: in outputs, there is only 1 'scripthash'
# type for either segwit/nonsegwit (hence "p2sh-p2wpkh"
@ -215,6 +218,13 @@ def pubkey_to_p2sh_p2wpkh_script(pub: bytes) -> CScript:
raise Exception("Invalid pubkey")
return pubkey_to_p2wpkh_script(pub).to_p2sh_scriptPubKey()
def pubkey_to_p2tr_script(pub: bytes) -> CScript:
"""
Given a pubkey in bytes (compressed), return a CScript
representing the corresponding pay-to-taproot scriptPubKey.
"""
return P2TRCoinAddress.from_pubkey(pub).to_scriptPubKey()
def redeem_script_to_p2wsh_script(redeem_script: Union[bytes, CScript]) -> CScript:
""" Given redeem script of type CScript (or bytes)
returns the corresponding segwit v0 scriptPubKey as
@ -246,12 +256,16 @@ def mk_burn_script(data: bytes) -> CScript:
raise TypeError("data must be in bytes")
return CScript([OP_RETURN, data])
def sign(tx: CMutableTransaction,
i: int,
priv: bytes,
hashcode: SIGHASH_Type = SIGHASH_ALL,
amount: Optional[int] = None,
native: bool = False) -> Tuple[Optional[bytes], str]:
def sign(
tx: CMutableTransaction,
i: int,
priv: bytes,
hashcode: SIGHASH_Type = SIGHASH_ALL,
amount: Optional[int] = None,
native: bool = False,
*,
spent_outputs: Optional[List[CTxOut]] = None
) -> Tuple[Optional[bytes], str]:
"""
Given a transaction tx of type CMutableTransaction, an input index i,
and a raw privkey in bytes, updates the CMutableTransaction to contain
@ -285,20 +299,44 @@ def sign(tx: CMutableTransaction,
tx.vin[i].scriptSig = CScript([sig, pub])
# Verify the signature worked.
try:
VerifyScript(tx.vin[i].scriptSig,
input_scriptPubKey, tx, i, flags=flags)
VerifyScript(
tx.vin[i].scriptSig, input_scriptPubKey, tx, i, flags=flags,
spent_outputs=spent_outputs)
except Exception as e:
return return_err(e)
return sig, "signing succeeded"
else:
# segwit case; we currently support p2wpkh native or under p2sh.
# segwit case; we currently support p2tr, p2wpkh native or under p2sh.
# https://github.com/Simplexum/python-bitcointx/blob/648ad8f45ff853bf9923c6498bfa0648b3d7bcbd/bitcointx/core/scripteval.py#L1250-L1252
flags.add(SCRIPT_VERIFY_P2SH)
flags.add(SCRIPT_VERIFY_WITNESS)
if native and native != "p2wpkh":
if native and native == "p2tr":
assert spent_outputs
sighash = SignatureHashSchnorr(tx, i, spent_outputs)
try:
coin_key = CCoinKey.from_secret_bytes(priv[:32])
sig = coin_key.sign_schnorr_tweaked(sighash)
except Exception as e:
return return_err(e)
witness = [sig]
ctxwitness = CTxInWitness(CScriptWitness(witness))
tx.wit.vtxinwit[i] = ctxwitness
try:
input_scriptPubKey = pubkey_to_p2tr_script(pub)
VerifyScript(
tx.vin[i].scriptSig, input_scriptPubKey, tx, i,
flags=flags, amount=amount,
witness=tx.wit.vtxinwit[i].scriptWitness,
spent_outputs=spent_outputs)
except ValidationError as e:
return return_err(e)
return sig, "signing succeeded"
elif native and native != "p2wpkh":
scriptCode = native
input_scriptPubKey = redeem_script_to_p2wsh_script(native)
else:
@ -328,8 +366,10 @@ def sign(tx: CMutableTransaction,
tx.wit.vtxinwit[i] = ctxwitness
# Verify the signature worked.
try:
VerifyScript(tx.vin[i].scriptSig, input_scriptPubKey, tx, i,
flags=flags, amount=amount, witness=tx.wit.vtxinwit[i].scriptWitness)
VerifyScript(
tx.vin[i].scriptSig, input_scriptPubKey, tx, i, flags=flags,
amount=amount, witness=tx.wit.vtxinwit[i].scriptWitness,
spent_outputs=spent_outputs)
except ValidationError as e:
return return_err(e)
@ -387,19 +427,23 @@ def make_shuffled_tx(ins: List[Tuple[bytes, int]],
return mktx(ins, outs, version=version, locktime=locktime)
def verify_tx_input(tx: CTransaction,
i: int,
scriptSig: CScript,
scriptPubKey: CScript,
amount: Optional[int] = None,
witness: Optional[CScriptWitness] = None) -> bool:
i: int,
scriptSig: CScript,
scriptPubKey: CScript,
amount: Optional[int] = None,
witness: Optional[CScriptWitness] = None,
*,
spent_outputs: Optional[List[CTxOut]] = None
) -> bool:
flags = set([SCRIPT_VERIFY_STRICTENC])
if witness:
# https://github.com/Simplexum/python-bitcointx/blob/648ad8f45ff853bf9923c6498bfa0648b3d7bcbd/bitcointx/core/scripteval.py#L1250-L1252
flags.add(SCRIPT_VERIFY_P2SH)
flags.add(SCRIPT_VERIFY_WITNESS)
try:
VerifyScript(scriptSig, scriptPubKey, tx, i,
flags=flags, amount=amount, witness=witness)
VerifyScript(
scriptSig, scriptPubKey, tx, i, flags=flags, amount=amount,
witness=witness, spent_outputs=spent_outputs)
except ValidationError as e:
return False
return True

4
src/jmclient/__init__.py

@ -17,7 +17,7 @@ from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportW
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin,
FidelityBondWatchonlyWallet, SegwitWalletFidelityBonds,
UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime,
UnknownAddressForLabel)
UnknownAddressForLabel, TaprootWallet)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage)
from .cryptoengine import (BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH, EngineError,
@ -27,7 +27,7 @@ from .configure import (load_test_config, process_shutdown,
load_program_config, jm_single, get_network, update_persist_config,
validate_address, is_burn_destination, get_mchannels,
get_blockchain_interface_instance, set_config, is_segwit_mode,
is_native_segwit_mode, JMPluginService, get_interest_rate,
is_taproot_mode, is_native_segwit_mode, JMPluginService, get_interest_rate,
get_bondless_makers_allowance, check_and_start_tor)
from .blockchaininterface import (BlockchainInterface,
RegtestBitcoinCoreInterface, BitcoinCoreInterface)

69
src/jmclient/blockchaininterface.py

@ -503,6 +503,49 @@ class BitcoinCoreInterface(BlockchainInterface):
self.import_addresses(addresses - imported_addresses, wallet_name)
return import_needed
def import_descriptors(self, desc_list: Iterable[str], wallet_name: str,
restart_cb: Callable[[str], None] = None) -> None:
requests = []
for desc in desc_list:
requests.append({
"desc": desc,
"timestamp": "now",
"label": wallet_name,
"internal": False
})
result = self._rpc('importdescriptors', [requests])
num_failed = 0
for row in result:
if row['success'] == False:
num_failed += 1
# don't try/catch, assume failure always has error message
log.warn(row['error']['message'])
if num_failed > 0:
fatal_msg = ("Fatal sync error: import of {} address(es) failed for "
"some reason. To prevent coin or privacy loss, "
"Joinmarket will not load a wallet in this conflicted "
"state. Try using a new Bitcoin Core wallet to sync this "
"Joinmarket wallet, or use a new Joinmarket wallet."
"".format(num_failed))
if restart_cb:
restart_cb(fatal_msg)
else:
jmprint(fatal_msg, "important")
sys.exit(EXIT_FAILURE)
def import_descriptors_if_needed(self, descriptors: Set[str], wallet_name: str) -> bool:
if wallet_name in self._rpc('listlabels', []):
imported_descriptors = set(self._rpc('getaddressesbylabel',
[wallet_name]).keys())
else:
imported_descriptors = set()
import_needed = not descriptors.issubset(imported_descriptors)
if import_needed:
self.import_descriptors(descriptors - imported_descriptors, wallet_name)
return import_needed
def get_deser_from_gettransaction(self, rpcretval: dict) -> Optional[btc.CMutableTransaction]:
if not "hex" in rpcretval:
log.info("Malformed gettransaction output")
@ -533,6 +576,22 @@ class BitcoinCoreInterface(BlockchainInterface):
return None
return res
def getrawtransaction(self, txid: bytes) -> Optional[dict]:
htxid = bintohex(txid)
try:
res = self._rpc("getrawtransaction", [htxid])
except JsonRpcError as e:
log.warn("Failed getrawtransaction call; JsonRpcError: " + repr(e))
return None
except Exception as e:
log.warn("Failed getrawtransaction call; unexpected error:")
log.warn(str(e))
return None
if res is None:
# happens in case of rpc connection failure:
return None
return res
def pushtx(self, txbin: bytes) -> bool:
""" Given a binary serialized valid bitcoin transaction,
broadcasts it to the network.
@ -720,6 +779,8 @@ class RegtestBitcoinCoreMixin():
if self._rpc('setgenerate', [True, reqd_blocks]):
raise Exception("Something went wrong")
"""
if not self.destn_addr:
self.destn_addr = self._rpc("getnewaddress", [])
# now we do a custom create transaction and push to the receiver
txid = self._rpc('sendtoaddress', [receiving_addr, amt])
if not txid:
@ -736,6 +797,7 @@ class BitcoinCoreNoHistoryInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixi
self.import_addresses_call_count = 0
self.wallet_name = None
self.scan_result = None
self.destn_addr = None
def import_addresses_if_needed(self, addresses: Set[str], wallet_name: str) -> bool:
self.import_addresses_call_count += 1
@ -793,7 +855,8 @@ class BitcoinCoreNoHistoryInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixi
wallet.disable_new_scripts = True
def tick_forward_chain(self, n: int) -> None:
self.destn_addr = self._rpc("getnewaddress", [])
if not self.destn_addr:
self.destn_addr = self._rpc("getnewaddress", [])
super().tick_forward_chain(n)
@ -810,7 +873,7 @@ class RegtestBitcoinCoreInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixin)
self.absurd_fees = False
self.simulating = False
self.shutdown_signal = False
self.destn_addr = self._rpc("getnewaddress", [])
self.destn_addr = None
def estimate_fee_per_kb(self, tx_fees: int) -> int:
if not self.absurd_fees:
@ -830,6 +893,8 @@ class RegtestBitcoinCoreInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixin)
self.tick_forward_chain(1)
def simulate_blocks(self) -> None:
if not self.destn_addr:
self.destn_addr = self._rpc("getnewaddress", [])
self.tickchainloop = task.LoopingCall(self.tickchain)
self.tickchainloop.start(self.tick_forward_chain_interval)
self.simulating = True

6
src/jmclient/configure.py

@ -953,6 +953,12 @@ def update_persist_config(section: str, name: str, value: Any) -> bool:
f.writelines([x.encode("utf-8") for x in newlines])
return True
def is_taproot_mode() -> bool:
c = jm_single().config
if not c.has_option('POLICY', 'taproot'):
return False
return c.get('POLICY', 'taproot') != 'false'
def is_segwit_mode() -> bool:
return jm_single().config.get('POLICY', 'segwit') != 'false'

31
src/jmclient/cryptoengine.py

@ -16,7 +16,8 @@ from .configure import get_network, jm_single
# make existing wallets unsable.
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \
TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, TYPE_P2TR = range(11)
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, \
TYPE_P2TR, TYPE_P2TR_FROST = range(12)
NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET,
'signet': NET_SIGNET}
@ -458,6 +459,32 @@ class BTC_Watchonly_P2WPKH(BTC_P2WPKH):
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")
class BTC_P2TR(BTCEngine):
@classproperty
def VBYTE(cls):
return btc.BTC_P2TR_VBYTE[get_network()]
@classmethod
def pubkey_to_script(cls, pubkey):
return btc.pubkey_to_p2tr_script(pubkey)
@classmethod
def pubkey_to_script_code(cls, pubkey):
raise NotImplementedError()
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
assert amount is not None
assert 'spent_outputs' in kwargs
spent_outputs = kwargs['spent_outputs']
return btc.sign(tx, index, privkey,
hashcode=hashcode, amount=amount, native="p2tr",
spent_outputs=spent_outputs)
ENGINES = {
TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
@ -466,5 +493,5 @@ ENGINES = {
TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH,
TYPE_WATCHONLY_P2WPKH: BTC_Watchonly_P2WPKH,
TYPE_SEGWIT_WALLET_FIDELITY_BONDS: BTC_P2WPKH,
TYPE_P2TR: None # TODO
TYPE_P2TR: BTC_P2TR,
}

49
src/jmclient/descriptor.py

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
GENERATOR = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd]
def descsum_polymod(symbols):
"""Internal function that computes the descriptor checksum."""
chk = 1
for value in symbols:
top = chk >> 35
chk = (chk & 0x7ffffffff) << 5 ^ value
for i in range(5):
chk ^= GENERATOR[i] if ((top >> i) & 1) else 0
return chk
def descsum_expand(s):
"""Internal function that does the character to symbol expansion"""
groups = []
symbols = []
for c in s:
if not c in INPUT_CHARSET:
return None
v = INPUT_CHARSET.find(c)
symbols.append(v & 31)
groups.append(v >> 5)
if len(groups) == 3:
symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
groups = []
if len(groups) == 1:
symbols.append(groups[0])
elif len(groups) == 2:
symbols.append(groups[0] * 3 + groups[1])
return symbols
def descsum_check(s):
"""Verify that the checksum is correct in a descriptor"""
if s[-9] != '#':
return False
if not all(x in CHECKSUM_CHARSET for x in s[-8:]):
return False
symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]]
return descsum_polymod(symbols) == 1
def descsum_create(s):
"""Add a checksum to a descriptor without"""
symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
checksum = descsum_polymod(symbols) ^ 1
return s + '#' + ''.join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8))

3
src/jmclient/maker.py

@ -171,6 +171,9 @@ class Maker(object):
elif self.wallet_service.get_txtype() == 'p2wpkh':
sig, pub = [a for a in iter(tx.wit.vtxinwit[index].scriptWitness)]
sigmsg = btc.CScript([sig]) + btc.CScript(pub)
elif self.wallet_service.get_txtype() == 'p2tr':
sig = [a for a in iter(tx.wit.vtxinwit[index].scriptWitness)]
sigmsg = btc.CScript(sig)
else:
jlog.error("Taker has unknown wallet type")
sys.exit(EXIT_FAILURE)

10
src/jmclient/support.py

@ -167,9 +167,9 @@ def select_one_utxo(unspent, value):
def calc_cj_fee(ordertype, cjfee, cj_amount):
if ordertype in ['sw0absoffer', 'swabsoffer', 'absoffer']:
if ordertype in ['trabsoffer', 'sw0absoffer', 'swabsoffer', 'absoffer']:
real_cjfee = int(cjfee)
elif ordertype in ['sw0reloffer', 'swreloffer', 'reloffer']:
elif ordertype in ['trreloffer', 'sw0reloffer', 'swreloffer', 'reloffer']:
real_cjfee = int((Decimal(cjfee) * Decimal(cj_amount)).quantize(Decimal(
1)))
else:
@ -338,9 +338,11 @@ def choose_sweep_orders(offers,
sumtxfee_contribution = 0
for order in ordercombo:
sumtxfee_contribution += order['txfee']
if order['ordertype'] in ['sw0absoffer', 'swabsoffer', 'absoffer']:
if order['ordertype'] in ['trabsoffer', 'sw0absoffer',
'swabsoffer', 'absoffer']:
sumabsfee += int(order['cjfee'])
elif order['ordertype'] in ['sw0reloffer', 'swreloffer', 'reloffer']:
elif order['ordertype'] in ['trreloffer', 'sw0reloffer',
'swreloffer', 'reloffer']:
sumrelfee += Decimal(order['cjfee'])
#this is unreachable since calc_cj_fee must already have been called
else: #pragma: no cover

36
src/jmclient/taker.py

@ -12,7 +12,8 @@ from jmbase import get_log, bintohex, hexbin
from jmclient.support import (calc_cj_fee, fidelity_bond_weighted_order_choose, choose_orders,
choose_sweep_orders)
from jmclient.wallet import (estimate_tx_fee, compute_tx_locktime,
FidelityBondMixin, UnknownAddressForLabel)
FidelityBondMixin, UnknownAddressForLabel,
TaprootWallet)
from jmclient.podle import generate_podle, get_podle_commitments
from jmclient.wallet_service import WalletService
from jmclient.fidelity_bond import FidelityBondProof
@ -284,6 +285,8 @@ class Taker(object):
allowed_types = ["swreloffer", "swabsoffer"]
elif self.wallet_service.get_txtype() == "p2wpkh":
allowed_types = ["sw0reloffer", "sw0absoffer"]
elif self.wallet_service.get_txtype() == "p2tr":
allowed_types = ["trreloffer", "trabsoffer"]
else:
jlog.error("Unrecognized wallet type, taker cannot continue.")
return False
@ -374,6 +377,8 @@ class Taker(object):
allowed_types = ["swreloffer", "swabsoffer"]
elif self.wallet_service.get_txtype() == "p2wpkh":
allowed_types = ["sw0reloffer", "sw0absoffer"]
elif self.wallet_service.get_txtype() == "p2tr":
allowed_types = ["trreloffer", "trabsoffer"]
else:
jlog.error("Unrecognized wallet type, taker cannot continue.")
return False
@ -679,8 +684,11 @@ class Taker(object):
jlog.debug("Junk signature: " + str(sig_deserialized) + \
", not attempting to verify")
break
# The second case here is kept for backwards compatibility.
if len(sig_deserialized) == 2:
# The third case here is kept for backwards compatibility.
if len(sig_deserialized) == 1:
ver_sig = sig_deserialized[0]
ver_pub = None
elif len(sig_deserialized) == 2:
ver_sig, ver_pub = sig_deserialized
elif len(sig_deserialized) == 3:
ver_sig, ver_pub, _ = sig_deserialized
@ -689,10 +697,17 @@ class Taker(object):
break
scriptPubKey = btc.CScript(utxo_data[i]['script'])
is_witness_input = scriptPubKey.is_p2sh() or scriptPubKey.is_witness_v0_keyhash()
is_witness_input = (scriptPubKey.is_p2sh()
or scriptPubKey.is_witness_v0_keyhash()
or scriptPubKey.is_witness_v1_taproot())
ver_amt = utxo_data[i]['value'] if is_witness_input else None
witness = btc.CScriptWitness(
[ver_sig, ver_pub]) if is_witness_input else None
if is_witness_input:
if ver_pub:
witness = btc.CScriptWitness([ver_sig, ver_pub])
else:
witness = btc.CScriptWitness([ver_sig])
else:
witness = None
# don't attempt to parse `pub` as pubkey unless it's valid.
if scriptPubKey.is_p2sh():
@ -704,13 +719,20 @@ class Taker(object):
if scriptPubKey.is_witness_v0_keyhash():
scriptSig = btc.CScript(b'')
elif scriptPubKey.is_witness_v1_taproot():
scriptSig = btc.CScript(b'')
elif scriptPubKey.is_p2sh():
scriptSig = btc.CScript([s])
else:
scriptSig = btc.CScript([ver_sig, ver_pub])
spent_outputs = None
wallet = self.wallet_service.wallet
if isinstance(wallet, TaprootWallet):
spent_outputs = wallet.get_spent_outputs(self.latest_tx)
sig_good = btc.verify_tx_input(self.latest_tx, u[0], scriptSig,
scriptPubKey, amount=ver_amt, witness=witness)
scriptPubKey, amount=ver_amt, witness=witness,
spent_outputs=spent_outputs)
if sig_good:
jlog.debug('found good sig at index=%d' % (u[0]))

95
src/jmclient/wallet.py

@ -29,11 +29,12 @@ from .support import select_gradual, select_greedy, select_greediest, \
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WSH,\
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\
TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, \
TYPE_WATCHONLY_P2WPKH, TYPE_P2TR, ENGINES, detect_script_type, EngineError
TYPE_WATCHONLY_P2WPKH, TYPE_P2TR, TYPE_P2TR_FROST, ENGINES, \
detect_script_type, EngineError
from .support import get_random_bytes
from . import mn_encode, mn_decode
import jmbitcoin as btc
from jmbase import JM_WALLET_NAME_PREFIX, bintohex
from jmbase import JM_WALLET_NAME_PREFIX, bintohex, hextobin
def _int_to_bytestr(i):
@ -513,6 +514,8 @@ class BaseWallet(object):
return 'p2pkh'
elif self.TYPE == TYPE_P2SH_P2WPKH:
return 'p2sh-p2wpkh'
elif self.TYPE == TYPE_P2TR:
return 'p2tr'
elif self.TYPE in (TYPE_P2WPKH,
TYPE_SEGWIT_WALLET_FIDELITY_BONDS):
return 'p2wpkh'
@ -540,6 +543,29 @@ class BaseWallet(object):
# from detect_script_type are covered.
assert False
def getrawtransaction(self, txid):
""" If the transaction for txid is an in-wallet
transaction, will return a CTransaction object for it;
if not, will return None.
"""
bci = jm_single().bc_interface
rawtx = bci.getrawtransaction(txid)
if not rawtx:
return None
return rawtx
def get_spent_outputs(self, tx):
spent_outputs = []
for txIn in tx.vin:
txid = txIn.prevout.hash[::-1]
n = txIn.prevout.n
rawtx = self.getrawtransaction(txid)
if rawtx:
prevtx = btc.CMutableTransaction.deserialize(
hextobin(rawtx))
spent_outputs.append(prevtx.vout[n])
return spent_outputs
def sign_tx(self, tx, scripts, **kwargs):
"""
Add signatures to transaction for inputs referenced by scripts.
@ -556,6 +582,11 @@ class BaseWallet(object):
assert amount > 0
path = self.script_to_path(script)
privkey, engine = self._get_key_from_path(path)
spent_outputs = None # need for SignatureHashSchnorr
if isinstance(self, TaprootWallet):
spent_outputs = self.get_spent_outputs(tx)
if spent_outputs:
kwargs['spent_outputs'] = spent_outputs
sig, msg = engine.sign_transaction(tx, index, privkey,
amount, **kwargs)
if not sig:
@ -577,14 +608,37 @@ class BaseWallet(object):
the wallet from other sources, or receiving payments or donations.
JoinMarket will never generate these addresses for internal use.
"""
return self.get_new_addr(mixdepth, self.ADDRESS_TYPE_EXTERNAL)
if isinstance(self, TaprootWallet):
pubkey = self.get_new_pubkey(mixdepth, self.ADDRESS_TYPE_EXTERNAL)
return self.pubkey_to_addr(pubkey)
else:
return self.get_new_addr(mixdepth, self.ADDRESS_TYPE_EXTERNAL)
def get_internal_addr(self, mixdepth):
"""
Return an address for internal usage, as change addresses and when
participating in transactions initiated by other parties.
"""
return self.get_new_addr(mixdepth, self.ADDRESS_TYPE_INTERNAL)
if isinstance(self, TaprootWallet):
pubkey = self.get_new_pubkey(mixdepth, self.ADDRESS_TYPE_INTERNAL)
return self.pubkey_to_addr(pubkey)
else:
return self.get_new_addr(mixdepth, self.ADDRESS_TYPE_INTERNAL)
def get_external_pubkey(self, mixdepth):
"""
Return an pubkey suitable for external distribution, including funding
the wallet from other sources, or receiving payments or donations.
JoinMarket will never generate these addresses for internal use.
"""
return self.get_new_pubkey(mixdepth, self.ADDRESS_TYPE_EXTERNAL)
def get_internal_pubkey(self, mixdepth):
"""
Return an pubkey for internal usage, as change addresses and when
participating in transactions initiated by other parties.
"""
return self.get_new_pubkey(mixdepth, self.ADDRESS_TYPE_INTERNAL)
def get_external_script(self, mixdepth):
return self.get_new_script(mixdepth, self.ADDRESS_TYPE_EXTERNAL)
@ -652,6 +706,12 @@ class BaseWallet(object):
return self.get_address_from_path(path,
validate_cache=validate_cache)
def get_pubkey(self, mixdepth, address_type, index,
validate_cache: bool = False):
path = self.get_path(mixdepth, address_type, index)
return self._get_pubkey_from_path(
path, validate_cache=validate_cache)[0]
def get_address_from_path(self, path,
validate_cache: bool = False):
cache = self._get_cache_for_path(path)
@ -680,6 +740,14 @@ class BaseWallet(object):
return self.script_to_addr(script,
validate_cache=validate_cache)
def get_new_pubkey(self, mixdepth, address_type,
validate_cache: bool = True):
script = self.get_new_script(
mixdepth, address_type, validate_cache=validate_cache)
path = self.script_to_path(script)
return self._get_pubkey_from_path(
path, validate_cache=validate_cache)[0]
def get_new_script(self, mixdepth, address_type,
validate_cache: bool = True):
raise NotImplementedError()
@ -2811,6 +2879,10 @@ class BIP84Wallet(BIP32PurposedWallet):
_PURPOSE = 2**31 + 84
_ENGINE = ENGINES[TYPE_P2WPKH]
class BIP86Wallet(BIP32PurposedWallet):
_PURPOSE = 2**31 + 86
_ENGINE = ENGINES[TYPE_P2TR]
class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, SNICKERWalletMixin, BIP49Wallet):
TYPE = TYPE_P2SH_P2WPKH
@ -2820,6 +2892,10 @@ class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, SNICKER
class SegwitWalletFidelityBonds(FidelityBondMixin, SegwitWallet):
TYPE = TYPE_SEGWIT_WALLET_FIDELITY_BONDS
class TaprootWallet(BIP39WalletMixin, BIP86Wallet):
# FIXME add other mixins if adapted
TYPE = TYPE_P2TR
class FidelityBondWatchonlyWallet(FidelityBondMixin, BIP84Wallet):
TYPE = TYPE_WATCHONLY_FIDELITY_BONDS
@ -2876,10 +2952,19 @@ class FidelityBondWatchonlyWallet(FidelityBondMixin, BIP84Wallet):
return pubkey, self._ENGINE
class FrostWallet(object):
_PURPOSE = 2**31 + 86
_ENGINE = ENGINES[TYPE_P2TR]
TYPE = TYPE_P2TR_FROST
WALLET_IMPLEMENTATIONS = {
LegacyWallet.TYPE: LegacyWallet,
SegwitLegacyWallet.TYPE: SegwitLegacyWallet,
SegwitWallet.TYPE: SegwitWallet,
SegwitWalletFidelityBonds.TYPE: SegwitWalletFidelityBonds,
FidelityBondWatchonlyWallet.TYPE: FidelityBondWatchonlyWallet
FidelityBondWatchonlyWallet.TYPE: FidelityBondWatchonlyWallet,
TaprootWallet.TYPE: TaprootWallet,
FrostWallet.TYPE: FrostWallet,
}

103
src/jmclient/wallet_service.py

@ -16,9 +16,10 @@ from jmclient.configure import jm_single, get_log
from jmclient.output import fmt_tx_data
from jmclient.blockchaininterface import (INF_HEIGHT, BitcoinCoreInterface,
BitcoinCoreNoHistoryInterface)
from jmclient.wallet import FidelityBondMixin, BaseWallet
from jmclient.wallet import FidelityBondMixin, BaseWallet, TaprootWallet
from jmbase import (stop_reactor, hextobin, utxo_to_utxostr,
jmprint, EXIT_SUCCESS, EXIT_FAILURE)
from .descriptor import descsum_create
"""Wallet service
@ -626,8 +627,21 @@ class WalletService(Service):
# we ensure that all addresses that will be displayed (see wallet_utils.py,
# function wallet_display()) are imported by importing gap limit beyond current
# index:
self.bci.import_addresses(self.collect_addresses_gap(), self.get_wallet_name(),
self.restart_callback)
if isinstance(self.wallet, TaprootWallet):
pubkeys = self.collect_pubkeys_gap()
desc_scripts = [f'tr({bytes(P)[1:].hex()})' for P in pubkeys]
descriptors = set()
for desc in desc_scripts:
descriptors.add(f'{descsum_create(desc)}')
self.bci.import_descriptors(
descriptors,
self.get_wallet_name(),
self.restart_callback)
else:
self.bci.import_addresses(
self.collect_addresses_gap(),
self.get_wallet_name(),
self.restart_callback)
if isinstance(self.wallet, FidelityBondMixin):
mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
@ -754,10 +768,19 @@ class WalletService(Service):
"""
jlog.debug("requesting detailed wallet history")
wallet_name = self.get_wallet_name()
addresses, saved_indices = self.collect_addresses_init()
import_needed = self.bci.import_addresses_if_needed(addresses,
wallet_name)
if isinstance(self.wallet, TaprootWallet):
pubkeys, saved_indices = self.collect_pubkeys_init()
desc_scripts = [f'tr({bytes(P)[1:].hex()})' for P in pubkeys]
descriptors= set()
for desc in desc_scripts:
descriptors.add(f'{descsum_create(desc)}')
import_needed = self.bci.import_descriptors_if_needed(
descriptors, wallet_name)
else:
addresses, saved_indices = self.collect_addresses_init()
import_needed = self.bci.import_addresses_if_needed(
addresses, wallet_name)
if import_needed:
self.display_rescan_message_and_system_exit(self.restart_callback)
return
@ -963,10 +986,22 @@ class WalletService(Service):
if self.bci is not None and hasattr(self.bci, 'import_addresses'):
self.bci.import_addresses([addr], self.wallet.get_wallet_name())
def import_pubkey(self, pubkey):
if self.bci is not None and hasattr(self.bci, 'import_descriptors'):
desc_script = f'tr({bytes(pubkey)[1:].hex()})'
descriptor = descsum_create(desc_script)
self.bci.import_descriptors(
[descriptor], self.wallet.get_wallet_name())
def get_internal_addr(self, mixdepth):
addr = self.wallet.get_internal_addr(mixdepth)
self.import_addr(addr)
return addr
if isinstance(self.wallet, TaprootWallet):
pubkey = self.wallet.get_internal_pubkey(mixdepth)
self.import_pubkey(pubkey)
return self.wallet.pubkey_to_addr(pubkey)
else:
addr = self.wallet.get_internal_addr(mixdepth)
self.import_addr(addr)
return addr
def collect_addresses_init(self) -> Tuple[Set[str], Dict[int, List[int]]]:
""" Collects the "current" set of addresses,
@ -1002,6 +1037,32 @@ class WalletService(Service):
addresses.add(self.get_addr(md, address_type, timenumber))
return addresses, saved_indices
def collect_pubkeys_init(self) -> Tuple[Set[str], Dict[int, List[int]]]:
""" Collects the "current" set of pubkeys,
as defined by the indices recorded in the wallet's
index cache (persisted in the wallet file usually).
Note that it collects up to the current indices plus
the gap limit.
"""
pubkeys = set()
saved_indices = dict()
for md in range(self.max_mixdepth + 1):
saved_indices[md] = [0, 0]
for address_type in (BaseWallet.ADDRESS_TYPE_EXTERNAL,
BaseWallet.ADDRESS_TYPE_INTERNAL):
next_unused = self.get_next_unused_index(md, address_type)
for index in range(next_unused):
pubkeys.add(self.get_pubkey(md, address_type, index))
for index in range(self.gap_limit):
pubkeys.add(self.get_new_pubkey(
md, address_type, validate_cache=False))
# reset the indices to the value we had before the
# new address calls:
self.set_next_index(md, address_type, next_unused)
saved_indices[md][address_type] = next_unused
return pubkeys, saved_indices
def collect_addresses_gap(self, gap_limit=None):
gap_limit = gap_limit or self.gap_limit
addresses = set()
@ -1015,10 +1076,28 @@ class WalletService(Service):
self.set_next_index(md, address_type, old_next)
return addresses
def collect_pubkeys_gap(self, gap_limit=None):
gap_limit = gap_limit or self.gap_limit
pubkeys = set()
for md in range(self.max_mixdepth + 1):
for address_type in (BaseWallet.ADDRESS_TYPE_INTERNAL,
BaseWallet.ADDRESS_TYPE_EXTERNAL):
old_next = self.get_next_unused_index(md, address_type)
for index in range(gap_limit):
pubkeys.add(self.get_new_pubkey(
md, address_type, validate_cache=False))
self.set_next_index(md, address_type, old_next)
return pubkeys
def get_external_addr(self, mixdepth):
addr = self.wallet.get_external_addr(mixdepth)
self.import_addr(addr)
return addr
if isinstance(self.wallet, TaprootWallet):
pubkey = self.wallet.get_external_pubkey(mixdepth)
self.import_pubkey(pubkey)
return self.wallet.pubkey_to_addr(pubkey)
else:
addr = self.wallet.get_external_addr(mixdepth)
self.import_addr(addr)
return addr
def __getattr__(self, attr):
# any method not present here is passed

45
src/jmclient/wallet_utils.py

@ -12,9 +12,10 @@ from itertools import islice, chain
from typing import Callable, Optional, Tuple, Union
from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle,
jm_single, WalletError, BaseWallet, VolatileStorage,
StoragePasswordError, is_segwit_mode, SegwitLegacyWallet, LegacyWallet,
SegwitWallet, FidelityBondMixin, FidelityBondWatchonlyWallet,
is_native_segwit_mode, load_program_config, add_base_options, check_regtest)
StoragePasswordError, is_taproot_mode, is_segwit_mode, SegwitLegacyWallet,
LegacyWallet, SegwitWallet, FidelityBondMixin, FidelityBondWatchonlyWallet,
TaprootWallet, is_native_segwit_mode, load_program_config,
add_base_options, check_regtest, JMClientProtocolFactory, start_reactor)
from jmclient.blockchaininterface import (BitcoinCoreInterface,
BitcoinCoreNoHistoryInterface)
from jmclient.wallet_service import WalletService
@ -24,9 +25,10 @@ from jmbase.support import (get_password, jmprint, EXIT_FAILURE,
cli_prompt_user_yesno)
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, \
TYPE_SEGWIT_WALLET_FIDELITY_BONDS
TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_P2TR
from .output import fmt_utxo
import jmbitcoin as btc
from .descriptor import descsum_create
# used for creating new wallets
@ -557,7 +559,12 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
path = wallet_service.get_path(m, address_type, k)
addr = wallet_service.get_address_from_path(path)
if k >= unused_index:
gap_addrs.append(addr)
if isinstance(wallet_service.wallet, TaprootWallet):
P = wallet_service.get_pubkey(m, address_type, k)
desc = f'tr({bytes(P)[1:].hex()})'
gap_addrs.append(f'{descsum_create(desc)}')
else:
gap_addrs.append(addr)
label = wallet_service.get_address_label(addr)
balance, status = get_addr_status(
path, utxos[m], utxos_enabled[m], k >= unused_index, address_type)
@ -577,8 +584,12 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
# displayed for user deposit.
# It also does not apply to fidelity bond addresses which are created manually.
if address_type == BaseWallet.ADDRESS_TYPE_EXTERNAL and wallet_service.bci is not None:
wallet_service.bci.import_addresses(gap_addrs,
wallet_service.get_wallet_name())
if isinstance(wallet_service.wallet, TaprootWallet):
wallet_service.bci.import_descriptors(
gap_addrs, wallet_service.get_wallet_name())
else:
wallet_service.bci.import_addresses(
gap_addrs, wallet_service.get_wallet_name())
wallet_service.set_next_index(m, address_type, unused_index)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
@ -755,7 +766,8 @@ def wallet_generate_recover_bip39(method: str,
password = enter_wallet_password_callback()
if not password:
return False
password = None
# return False
wallet_name = enter_wallet_file_name_callback()
if wallet_name == "cancelled":
@ -765,7 +777,10 @@ def wallet_generate_recover_bip39(method: str,
if not wallet_name:
wallet_name = default_wallet_name
wallet_path = os.path.join(walletspath, wallet_name)
support_fidelity_bonds = enter_do_support_fidelity_bonds()
if is_taproot_mode():
support_fidelity_bonds = False
else:
support_fidelity_bonds = enter_do_support_fidelity_bonds()
wallet_cls = get_wallet_cls(get_configured_wallet_type(support_fidelity_bonds))
wallet = create_wallet(wallet_path, password, mixdepth, wallet_cls,
entropy=entropy,
@ -779,7 +794,13 @@ def wallet_generate_recover_bip39(method: str,
def wallet_generate_recover(method, walletspath,
default_wallet_name='wallet.jmdat',
mixdepth=DEFAULT_MIXDEPTH):
if is_segwit_mode():
if is_taproot_mode():
return wallet_generate_recover_bip39(method, walletspath,
default_wallet_name, cli_display_user_words, cli_user_mnemonic_entry,
cli_get_wallet_passphrase_check, cli_get_wallet_file_name,
cli_do_use_mnemonic_extension, cli_get_mnemonic_extension,
cli_do_support_fidelity_bonds, mixdepth=mixdepth)
elif is_segwit_mode():
#Here using default callbacks for scripts (not used in Qt)
return wallet_generate_recover_bip39(method, walletspath,
default_wallet_name, cli_display_user_words, cli_user_mnemonic_entry,
@ -1429,7 +1450,9 @@ def wallet_createwatchonly(wallet_root_path, master_pub_key):
def get_configured_wallet_type(support_fidelity_bonds):
configured_type = TYPE_P2PKH
if is_segwit_mode():
if is_taproot_mode():
return TYPE_P2TR
elif is_segwit_mode():
if is_native_segwit_mode():
configured_type = TYPE_P2WPKH
else:

10
src/jmclient/yieldgenerator.py

@ -99,13 +99,15 @@ class YieldGeneratorBasic(YieldGenerator):
max_mix = max(mix_balance, key=mix_balance.get)
f = '0'
if self.ordertype in ('reloffer', 'swreloffer', 'sw0reloffer'):
if self.ordertype in ('reloffer', 'swreloffer',
'sw0reloffer', 'trreloffer'):
f = self.cjfee_r
#minimum size bumped if necessary such that you always profit
#least 50% of the miner fee
self.minsize = max(int(1.5 * self.txfee_contribution /
float(self.cjfee_r)), self.minsize)
elif self.ordertype in ('absoffer', 'swabsoffer', 'sw0absoffer'):
elif self.ordertype in ('absoffer', 'swabsoffer',
'sw0absoffer', 'trreloffer'):
f = str(self.txfee_contribution + self.cjfee_a)
order = {'oid': 0,
'ordertype': self.ordertype,
@ -455,7 +457,9 @@ def ygmain(ygclass, nickserv_password='', gaplimit=6):
wallet_service.startService()
txtype = wallet_service.get_txtype()
if txtype == "p2wpkh":
if txtype == "p2tr":
prefix = "tr"
elif txtype == "p2wpkh":
prefix = "sw0"
elif txtype == "p2sh-p2wpkh":
prefix = "sw"

5
src/jmdaemon/orderbookwatch.py

@ -96,8 +96,9 @@ class OrderbookWatch(object):
"from {}").format
log.debug(fmt(minsize, maxsize, counterparty))
return
if ordertype in ['sw0absoffer', 'swabsoffer', 'absoffer']\
and not isinstance(cjfee, Integral):
if (ordertype in ['trabsoffer', 'sw0absoffer',
'swabsoffer', 'absoffer']
and not isinstance(cjfee, Integral)):
try:
cjfee = int(cjfee)
except ValueError:

4
src/jmdaemon/protocol.py

@ -16,6 +16,10 @@ offertypes = {"reloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"),
"sw0reloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"),
(int, "txfee"), (float, "cjfee")],
"sw0absoffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"),
(int, "txfee"), (int, "cjfee")],
"trreloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"),
(int, "txfee"), (float, "cjfee")],
"trabsoffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"),
(int, "txfee"), (int, "cjfee")]}
offername_list = list(offertypes.keys())

22
test/jmclient/test_wallet.py

@ -14,7 +14,7 @@ from jmclient import load_test_config, jm_single, BaseWallet, \
SegwitWallet, WalletService, SegwitWalletFidelityBonds,\
create_wallet, open_test_wallet_maybe, open_wallet, \
FidelityBondMixin, FidelityBondWatchonlyWallet,\
wallet_gettimelockaddress, UnknownAddressForLabel
wallet_gettimelockaddress, UnknownAddressForLabel, TaprootWallet
from test_blockchaininterface import sync_test_wallet
from freezegun import freeze_time
@ -414,6 +414,26 @@ def test_signing_simple(setup_wallet, wallet_cls, type_check):
txout = jm_single().bc_interface.pushtx(tx.serialize())
assert txout
def test_signing_simple_p2tr(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
storage = VolatileStorage()
TaprootWallet.initialize(storage, get_network(), entropy=b"\xaa"*16)
wallet = TaprootWallet(storage)
utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(0))
# The dummy output is constructed as an unspendable p2sh:
tx = btc.mktx([utxo],
[{"address": str(btc.CCoinAddress.from_scriptPubKey(
btc.CScript(b"\x00").to_p2sh_scriptPubKey())),
"value": 10**8 - 9000}])
script = wallet.get_script(0, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
assert success, msg
assert_segwit(tx)
txout = jm_single().bc_interface.pushtx(tx.serialize())
assert txout
# note that address validation is tested separately;
# this test functions only to make sure that given a valid
# taproot address, we can actually spend to it

Loading…
Cancel
Save