Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor wallet creation to Wallet.create method #1242

Merged
merged 8 commits into from
Jun 20, 2021
138 changes: 138 additions & 0 deletions src/cryptoadvance/specter/liquid/wallet.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,150 @@
from ..wallet import *
from ..addresslist import Address
from embit import ec
from embit.liquid.pset import PSET
from embit.liquid.transaction import LTransaction


class LWallet(Wallet):
MIN_FEE_RATE = 0.1

@classmethod
def create(
cls,
rpc,
rpc_path,
working_folder,
device_manager,
wallet_manager,
name,
alias,
sigs_required,
key_type,
keys,
devices,
core_version=None,
):
"""Creates a wallet. If core_version is not specified - get it from rpc"""
# get xpubs in a form [fgp/der]xpub from all keys
xpubs = [key.metadata["combined"] for key in keys]
recv_keys = ["%s/0/*" % xpub for xpub in xpubs]
change_keys = ["%s/1/*" % xpub for xpub in xpubs]
is_multisig = len(keys) > 1
# we start by constructing an argument for descriptor wrappers
if is_multisig:
recv_descriptor = "sortedmulti({},{})".format(
sigs_required, ",".join(recv_keys)
)
change_descriptor = "sortedmulti({},{})".format(
sigs_required, ",".join(change_keys)
)
else:
recv_descriptor = recv_keys[0]
change_descriptor = change_keys[0]
# now we iterate over script-type in reverse order
# to get sh(wpkh(xpub)) from sh-wpkh and xpub
arr = key_type.split("-")
for el in arr[::-1]:
recv_descriptor = "%s(%s)" % (el, recv_descriptor)
change_descriptor = "%s(%s)" % (el, change_descriptor)

# get blinding key for the wallet
blinding_key = None
if len(devices) == 1:
blinding_key = devices[0].blinding_key
# if we don't have slip77 key for a device or it is multisig
# we use chaincodes to generate slip77 key.
if not blinding_key:
desc = LDescriptor.from_string(recv_descriptor)
# For now we use sha256(b"blinding_key", xor(chaincodes)) as a blinding key
# where chaincodes are corresponding to xpub of the first receiving address.
# It's not a standard but we use that until musig(blinding_xpubs) is implemented.
# Chaincodes of the first address are not used anywhere else so they can be used
# as a source for the blinding keys. They are also independent of the xpub's origin.
xor = bytearray(32)
desc_keys = desc.derive(0).keys
for k in desc_keys:
if k.is_extended:
chaincode = k.key.chain_code
for i in range(32):
xor[i] = xor[i] ^ chaincode[i]
secret = hashlib.sha256(b"blinding_key" + bytes(xor)).digest()
blinding_key = ec.PrivateKey(secret).wif()
if blinding_key:
recv_descriptor = f"blinded(slip77({blinding_key}),{recv_descriptor})"
change_descriptor = f"blinded(slip77({blinding_key}),{change_descriptor})"

recv_descriptor = AddChecksum(recv_descriptor)
change_descriptor = AddChecksum(change_descriptor)
assert recv_descriptor != change_descriptor

# get Core version if we don't know it
if core_version is None:
core_version = rpc.getnetworkinfo().get("version", 0)

use_descriptors = core_version >= 209900
# v20.99 is pre-v21 Elements Core for descriptors
if use_descriptors:
# Use descriptor wallet
rpc.createwallet(os.path.join(rpc_path, alias), True, True, "", False, True)
else:
rpc.createwallet(os.path.join(rpc_path, alias), True)

wallet_rpc = rpc.wallet(os.path.join(rpc_path, alias))
# import descriptors
args = [
{
"desc": desc,
"internal": change,
"timestamp": "now",
"watchonly": True,
}
for (change, desc) in [(False, recv_descriptor), (True, change_descriptor)]
]
for arg in args:
if use_descriptors:
arg["active"] = True
else:
arg["keypool"] = True
arg["range"] = [0, cls.GAP_LIMIT]

assert args[0] != args[1]

# Descriptor wallets were introduced in v0.21.0, but upgraded nodes may
# still have legacy wallets. Use getwalletinfo to check the wallet type.
# The "keypool" for descriptor wallets is automatically refilled
if use_descriptors:
res = wallet_rpc.importdescriptors(args)
else:
res = wallet_rpc.importmulti(args, {"rescan": False})

assert all([r["success"] for r in res])

return cls(
name,
alias,
"{} of {} {}".format(sigs_required, len(keys), purposes[key_type])
if len(keys) > 1
else purposes[key_type],
addrtypes[key_type],
"",
-1,
"",
-1,
0,
0,
recv_descriptor,
change_descriptor,
keys,
devices,
sigs_required,
{},
[],
os.path.join(working_folder, "%s.json" % alias),
device_manager,
wallet_manager,
)

def get_balance(self):
try:
full_balance = (
Expand Down
104 changes: 10 additions & 94 deletions src/cryptoadvance/specter/managers/wallet_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..rpc import RpcError, get_default_datadir
from ..specter_error import SpecterError
from ..util.descriptor import AddChecksum
from ..wallet import Wallet
from ..wallet import Wallet, purposes
from ..liquid.wallet import LWallet

from embit import ec
Expand All @@ -34,27 +34,6 @@

logger = logging.getLogger(__name__)

purposes = OrderedDict(
{
None: "General",
"wpkh": "Single (Segwit)",
"sh-wpkh": "Single (Nested)",
"pkh": "Single (Legacy)",
"wsh": "Multisig (Segwit)",
"sh-wsh": "Multisig (Nested)",
"sh": "Multisig (Legacy)",
}
)

addrtypes = {
"pkh": "legacy",
"sh-wpkh": "p2sh-segwit",
"wpkh": "bech32",
"sh": "legacy",
"sh-wsh": "p2sh-segwit",
"wsh": "bech32",
}


class WalletManager:
# chain is required to manage wallets when bitcoind is not running
Expand Down Expand Up @@ -322,82 +301,19 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices):
wallet_alias = alias("%s %d" % (name, i))
i += 1

arr = key_type.split("-")
descs = [key.metadata["combined"] for key in keys]
recv_descs = ["%s/0/*" % desc for desc in descs]
change_descs = ["%s/1/*" % desc for desc in descs]
if len(keys) > 1:
recv_descriptor = "sortedmulti({},{})".format(
sigs_required, ",".join(recv_descs)
)
change_descriptor = "sortedmulti({},{})".format(
sigs_required, ",".join(change_descs)
)
else:
recv_descriptor = recv_descs[0]
change_descriptor = change_descs[0]
for el in arr[::-1]:
recv_descriptor = "%s(%s)" % (el, recv_descriptor)
change_descriptor = "%s(%s)" % (el, change_descriptor)

if is_liquid(self.chain):
blinding_key = None
if len(devices) == 1:
blinding_key = devices[0].blinding_key
if not blinding_key:
desc = Descriptor.from_string(recv_descriptor.split("#")[0])
# For now we use sha256(b"blinding_key", xor(chaincodes)) as a blinding key
# where chaincodes are corresponding to xpub of the first receiving address
xor = bytearray(32)
desc_keys = desc.derive(0).keys
for k in desc_keys:
if k.is_extended:
chaincode = k.key.chain_code
for i in range(32):
xor[i] = xor[i] ^ chaincode[i]
secret = hashlib.sha256(b"blinding_key" + bytes(xor)).digest()
blinding_key = ec.PrivateKey(secret).wif()
if blinding_key:
recv_descriptor = f"blinded(slip77({blinding_key}),{recv_descriptor})"
change_descriptor = (
f"blinded(slip77({blinding_key}),{change_descriptor})"
)

recv_descriptor = AddChecksum(recv_descriptor)
change_descriptor = AddChecksum(change_descriptor)

# v20.99 is pre-v21 Elements Core for descriptors
if self.bitcoin_core_version_raw >= 209900:
# Use descriptor wallet
self.rpc.createwallet(
os.path.join(self.rpc_path, wallet_alias), True, True, "", False, True
)
else:
self.rpc.createwallet(os.path.join(self.rpc_path, wallet_alias), True)

w = self.WalletClass(
w = self.WalletClass.create(
self.rpc,
self.rpc_path,
self.working_folder,
self.device_manager,
self,
name,
wallet_alias,
"{} of {} {}".format(sigs_required, len(keys), purposes[key_type])
if len(keys) > 1
else purposes[key_type],
addrtypes[key_type],
"",
-1,
"",
-1,
0,
0,
recv_descriptor,
change_descriptor,
sigs_required,
key_type,
keys,
devices,
sigs_required,
{},
[],
os.path.join(self.working_folder, "%s.json" % wallet_alias),
self.device_manager,
self,
self.bitcoin_core_version_raw,
)
# save wallet file to disk
if w and self.working_folder is not None:
Expand Down
2 changes: 1 addition & 1 deletion src/cryptoadvance/specter/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def test_rpc(self):
r["code"] = 0
try:
r["tests"]["recent_version"] = (
int(rpc.getnetworkinfo()["version"]) >= 170000
int(rpc.getnetworkinfo()["version"]) >= 200000
)
if not r["tests"]["recent_version"]:
r["err"] = "Core Node might be too old"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
{% if specter.info["initialblockdownload"] %}
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>Bitcoin Core is still syncing...<br>(data might be outdated)</p>
{% endif %}
{% if specter.network_info.version < 190000 %}
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>Bitcoin Core version is outdated.<br>Some features might not work...<br>(minimum required: v19.0.0).</p>
{% if specter.network_info.version < 200000 %}
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>Bitcoin Core version is outdated.<br>Some features might not work...<br>(minimum required: v20.0.0).</p>
{% endif %}
<div
id="active-node" onclick="showPageOverlay('bitcoin_core_info');document.getElementById('side-content').classList.remove('active');"
Expand Down
Loading