Revert active hdchains.

This commit is contained in:
tecnovert
2025-04-12 00:36:10 +02:00
parent 3e98f174cd
commit 6bc654f57e
6 changed files with 829 additions and 35 deletions

View File

@@ -7,25 +7,42 @@
import json
import logging
import mmap
import multiprocessing
import os
import shlex
import shutil
import sqlite3
import subprocess
import sys
import threading
import unittest
from unittest.mock import patch
from basicswap.util.ecc import (
i2b,
getSecretInt,
)
from basicswap.rpc import escape_rpcauth, make_rpc_func
from basicswap.interface.dcr.rpc import make_rpc_func as make_dcr_rpc_func
from tests.basicswap.util import (
read_json_api,
waitForServer,
)
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap.util.address import (
b58encode,
toWIF,
)
from basicswap.util.crypto import (
sha256,
)
from basicswap.util.extkey import ExtKeyPair
from basicswap.contrib.test_framework.descriptors import descsum_create
bin_path = os.path.expanduser(os.getenv("TEST_BIN_PATH", ""))
test_base_path = os.path.expanduser(os.getenv("TEST_PATH", "~/test_basicswap"))
test_base_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_basicswap"))
delay_event = threading.Event()
logger = logging.getLogger()
@@ -75,6 +92,21 @@ def callcoincli(binpath, datadir, params, wallet=None, timeout=None):
return out[0].decode("utf-8").strip()
def encode_secret_extkey(prefix, ek_data: bytes) -> str:
assert len(ek_data) == 74
data: bytes = prefix.to_bytes(4, "big") + ek_data
checksum = sha256(sha256(data))
return b58encode(data + checksum[0:4])
test_seed: bytes = bytes.fromhex(
"8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
)
test_seedid = "3da5c0af91879e8ce97d9a843874601c08688078"
wif_prefix: int = 239
test_wif: str = toWIF(wif_prefix, test_seed)
class Test(unittest.TestCase):
test_coins = [
@@ -594,6 +626,7 @@ class Test(unittest.TestCase):
assert "success" in rv
for coin in self.test_coins:
logging.info(f"Coin: {coin}")
if coin == "particl":
continue
if coin == "firo":
@@ -658,6 +691,588 @@ class Test(unittest.TestCase):
process.join()
assert process.exitcode == 0
def write_btc_conf(self, datadir):
conf_path = os.path.join(datadir, "bitcoin.conf")
with open(conf_path, "w") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("printtoconsole=0\n")
fp.write("server=1\n")
fp.write("discover=0\n")
fp.write("listenonion=0\n")
fp.write("bind=127.0.0.1\n")
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("deprecatedrpc=create_bdb\n")
fp.write("rpcport=12223\n")
salt = generate_salt(16)
fp.write(
"rpcauth={}:{}${}\n".format(
"test",
salt,
password_to_hmac(salt, "test_pass"),
)
)
def test_btc_wallets_with_module(self):
import berkeleydb
if not os.path.exists(bin_path):
raise ValueError("TEST_BIN_PATH not set.")
test_path = os.path.join(test_base_path, "test_btc_wallets_with_module")
if os.path.exists(test_path):
shutil.rmtree(test_path)
os.makedirs(test_path)
self.write_btc_conf(test_path)
daemon_path = os.path.join(bin_path, "bitcoin", "bitcoind")
args = [
daemon_path,
"-datadir=" + test_path,
]
bitcoind_process = subprocess.Popen(args)
try:
rpc = make_rpc_func(12223, "test:test_pass")
for i in range(20):
try:
rv = rpc("listwallets")
break
except Exception as e: # noqa: F841
delay_event.wait(1.0)
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
rpc(
"createwallet",
["bdb_wallet", False, True, "", False, False],
)
rpc("sethdseed", [True, test_wif], wallet_override="bdb_wallet")
rv = rpc("getwalletinfo", wallet_override="bdb_wallet")
assert rv["hdseedid"] == test_seedid
new_addr = rpc("getnewaddress", wallet_override="bdb_wallet")
logging.info(f"getnewaddress {new_addr}")
rv = rpc(
"getaddressinfo",
[
new_addr,
],
wallet_override="bdb_wallet",
)
logging.info(f"getaddressinfo before encrypt {rv}")
assert rv["hdmasterfingerprint"] == "a55b7ea9"
rpc(
"unloadwallet",
[
"bdb_wallet",
],
)
walletdir = os.path.join(test_path, "regtest", "wallets", "bdb_wallet")
walletpath = os.path.join(walletdir, "wallet.dat")
db = berkeleydb.db.DB()
db.open(
walletpath,
"main",
berkeleydb.db.DB_BTREE,
berkeleydb.db.DB_THREAD | berkeleydb.db.DB_CREATE,
)
prev_hdchain_key = None
prev_hdchain_value = None
for k, v in db.items():
if b"hdchain" in k:
prev_hdchain_key = k
prev_hdchain_value = v
db.close()
rpc(
"loadwallet",
[
"bdb_wallet",
],
)
rv = rpc(
"encryptwallet",
[
"test.123",
],
wallet_override="bdb_wallet",
)
logging.info(f"encryptwallet {rv}")
rv = rpc("getwalletinfo", wallet_override="bdb_wallet")
logging.info(f"getwalletinfo {rv}")
assert rv["hdseedid"] != test_seedid
rpc(
"unloadwallet",
[
"bdb_wallet",
],
)
bkp_path = os.path.join(walletdir, "wallet.dat" + ".bkp")
for i in range(1000):
if os.path.exists(bkp_path):
bkp_path = os.path.join(walletdir, "wallet.dat" + f".bkp{i}")
assert os.path.exists(bkp_path) is False
if os.path.isfile(walletpath):
shutil.copy(walletpath, bkp_path)
else:
shutil.copytree(walletpath, bkp_path)
# Replace hdchain with previous value
db = berkeleydb.db.DB()
db.open(
walletpath,
"main",
berkeleydb.db.DB_BTREE,
berkeleydb.db.DB_THREAD | berkeleydb.db.DB_CREATE,
)
db[prev_hdchain_key] = prev_hdchain_value
db.close()
rpc(
"loadwallet",
[
"bdb_wallet",
],
)
rv = rpc("getwalletinfo", wallet_override="bdb_wallet")
logging.info(f"getwalletinfo {rv}")
assert rv["hdseedid"] == test_seedid
# Picks from wrong keypool
new_addr = rpc("getnewaddress", wallet_override="bdb_wallet")
rv = rpc(
"getaddressinfo",
[
new_addr,
],
wallet_override="bdb_wallet",
)
assert rv["hdmasterfingerprint"] != "a55b7ea9"
rpc("walletpassphrase", ["test.123", 1000], wallet_override="bdb_wallet")
rpc("newkeypool", wallet_override="bdb_wallet")
new_addr = rpc("getnewaddress", wallet_override="bdb_wallet")
rv = rpc(
"getaddressinfo",
[
new_addr,
],
wallet_override="bdb_wallet",
)
assert rv["hdmasterfingerprint"] == "a55b7ea9"
finally:
rpc("stop")
bitcoind_process.wait(timeout=30)
def test_btc_wallets_without_module(self):
if not os.path.exists(bin_path):
raise ValueError("TEST_BIN_PATH not set.")
test_path = os.path.join(test_base_path, "test_btc_wallets_without_module")
if os.path.exists(test_path):
shutil.rmtree(test_path)
os.makedirs(test_path)
self.write_btc_conf(test_path)
daemon_path = os.path.join(bin_path, "bitcoin", "bitcoind")
args = [
daemon_path,
"-datadir=" + test_path,
]
bitcoind_process = subprocess.Popen(args)
try:
rpc = make_rpc_func(12223, "test:test_pass")
for i in range(20):
try:
rv = rpc("listwallets")
break
except Exception as e: # noqa: F841
delay_event.wait(1.0)
rv = rpc(
"createwallet",
["bdb_wallet2", False, True, "", False, False],
)
logging.info(f"createwallet {rv}")
rpc("sethdseed", [True, test_wif], wallet_override="bdb_wallet2")
new_addr = rpc("getnewaddress", wallet_override="bdb_wallet2")
logging.info(f"getnewaddress {new_addr}")
rv = rpc(
"getaddressinfo",
[
new_addr,
],
wallet_override="bdb_wallet2",
)
logging.info(f"getaddressinfo before encrypt {rv}")
assert rv["hdmasterfingerprint"] == "a55b7ea9"
rv = rpc("getwalletinfo", wallet_override="bdb_wallet2")
logging.info(f"getwalletinfo {rv}")
assert rv["hdseedid"] == test_seedid
seedid_bytes = bytes.fromhex(rv["hdseedid"])[::-1]
# keypoolsize and keypoolsize_hd_internal are how many pre-generated keys remain unused.
orig_hdchain_bytes_predicted = (
int(2).to_bytes(4, "little")
+ int(1000).to_bytes(4, "little")
+ seedid_bytes
+ int(1000).to_bytes(4, "little")
)
logging.info(
f"orig_hdchain_bytes_predicted {orig_hdchain_bytes_predicted.hex()}"
)
rv = rpc(
"unloadwallet",
[
"bdb_wallet2",
],
)
logging.info(f"Looking for hdchain for {seedid_bytes.hex()}")
walletdir = os.path.join(test_path, "regtest", "wallets", "bdb_wallet2")
walletpath = os.path.join(walletdir, "wallet.dat")
found_hdchain = False
max_key_count = 4000000 # arbitrary
with open(walletpath, "rb") as fp:
with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as mm:
pos = mm.find(seedid_bytes)
while pos != -1:
mm.seek(pos - 8)
hdchain_bytes = mm.read(12 + 20)
version = int.from_bytes(hdchain_bytes[:4], "little")
if version == 2:
external_counter = int.from_bytes(
hdchain_bytes[4:8], "little"
)
internal_counter = int.from_bytes(
hdchain_bytes[-4:], "little"
)
if (
external_counter > 0
and external_counter <= max_key_count
and internal_counter > 0
and internal_counter <= max_key_count
):
orig_hdchain_bytes = hdchain_bytes
found_hdchain = True
break
pos = mm.find(seedid_bytes, pos + 1)
logging.info(f"orig_hdchain_bytes {orig_hdchain_bytes.hex()}")
assert found_hdchain
rpc(
"loadwallet",
[
"bdb_wallet2",
],
)
rv = rpc(
"encryptwallet",
[
"test.123",
],
wallet_override="bdb_wallet2",
)
logging.info(f"encryptwallet {rv}")
rv = rpc("getwalletinfo", wallet_override="bdb_wallet2")
logging.info(f"getwalletinfo {rv}")
assert rv["hdseedid"] != test_seedid
new_hdchain_bytes = (
int(2).to_bytes(4, "little")
+ int(rv["keypoolsize"]).to_bytes(4, "little")
+ bytes.fromhex(rv["hdseedid"])[::-1]
+ int(rv["keypoolsize_hd_internal"]).to_bytes(4, "little")
)
rpc(
"unloadwallet",
[
"bdb_wallet2",
],
)
with open(walletpath, "r+b") as fp:
with mmap.mmap(fp.fileno(), 0) as mm:
offset = mm.find(new_hdchain_bytes)
if offset != -1:
mm.seek(offset)
mm.write(orig_hdchain_bytes)
print(f"Replaced at offset: {offset}")
else:
print("Byte sequence not found.")
rpc(
"loadwallet",
[
"bdb_wallet2",
],
)
rv = rpc("getwalletinfo", wallet_override="bdb_wallet2")
logging.info(f"getwalletinfo {rv}")
assert rv["hdseedid"] == test_seedid
rpc("walletpassphrase", ["test.123", 1000], wallet_override="bdb_wallet2")
rpc("newkeypool", wallet_override="bdb_wallet2")
new_addr = rpc("getnewaddress", wallet_override="bdb_wallet2")
rv = rpc(
"getaddressinfo",
[
new_addr,
],
wallet_override="bdb_wallet2",
)
assert rv["hdmasterfingerprint"] == "a55b7ea9"
finally:
rpc("stop")
bitcoind_process.wait(timeout=30)
def test_btc_wallets_descriptors(self):
if not os.path.exists(bin_path):
raise ValueError("TEST_BIN_PATH not set.")
test_path = os.path.join(test_base_path, "test_btc_wallets_descriptors")
if os.path.exists(test_path):
shutil.rmtree(test_path)
os.makedirs(test_path)
self.write_btc_conf(test_path)
daemon_path = os.path.join(bin_path, "bitcoin", "bitcoind")
args = [
daemon_path,
"-datadir=" + test_path,
]
bitcoind_process = subprocess.Popen(args)
try:
rpc = make_rpc_func(12223, "test:test_pass")
for i in range(20):
try:
rv = rpc("listwallets")
break
except Exception as e: # noqa: F841
delay_event.wait(1.0)
rpc(
"createwallet",
["descr_wallet", False, True, "", False, True],
)
ek = ExtKeyPair()
ek.set_seed(test_seed)
ek_encoded: str = encode_secret_extkey(0x04358394, ek.encode_v())
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
rv = rpc(
"importdescriptors",
[
[
{
"desc": desc_external,
"timestamp": "now",
"active": True,
"range": [0, 10],
"next_index": 0,
},
{
"desc": desc_internal,
"timestamp": "now",
"active": True,
"internal": True,
},
],
],
wallet_override="descr_wallet",
)
logging.info(f"importdescriptors {rv}")
addr = rpc(
"getnewaddress", ["test descriptors"], wallet_override="descr_wallet"
)
addr_info = rpc(
"getaddressinfo",
[
addr,
],
wallet_override="descr_wallet",
)
assert addr_info["hdmasterfingerprint"] == "a55b7ea9"
descriptors_before = rpc(
"listdescriptors", [], wallet_override="descr_wallet"
)
logging.info(f"descriptors_before {descriptors_before}")
rv = rpc("getwalletinfo", wallet_override="descr_wallet")
logging.info(f"getwalletinfo {rv}")
new_addr = rpc("getnewaddress", wallet_override="descr_wallet")
rv = rpc(
"getaddressinfo",
[
new_addr,
],
wallet_override="descr_wallet",
)
assert rv["hdmasterfingerprint"] == "a55b7ea9"
rpc(
"unloadwallet",
[
"descr_wallet",
],
)
walletdir = os.path.join(test_path, "regtest", "wallets", "descr_wallet")
walletpath = os.path.join(walletdir, "wallet.dat")
orig_active_descriptors = []
with sqlite3.connect(walletpath) as conn:
c = conn.cursor()
rows = c.execute(
"SELECT * FROM main WHERE key in (:kext, :kint)",
{
"kext": bytes.fromhex("1161637469766565787465726e616c73706b02"),
"kint": bytes.fromhex("11616374697665696e7465726e616c73706b02"),
},
)
for row in rows:
k, v = row
orig_active_descriptors.append({"k": k, "v": v})
assert len(orig_active_descriptors) == 2
rpc(
"loadwallet",
[
"descr_wallet",
],
)
rv = rpc(
"encryptwallet",
[
"test.123",
],
wallet_override="descr_wallet",
)
logging.info(f"encryptwallet {rv}")
rv = rpc("listdescriptors", [], wallet_override="descr_wallet")
logging.info(f"listdescriptors {rv}")
# The descriptors don't seem to be replaced
addr = rpc(
"getnewaddress", ["test descriptors"], wallet_override="descr_wallet"
)
addr_info = rpc(
"getaddressinfo",
[
addr,
],
wallet_override="descr_wallet",
)
assert addr_info["hdmasterfingerprint"] == "a55b7ea9"
# Simulate, in case it changes
rpc("walletpassphrase", ["test.123", 1000], wallet_override="descr_wallet")
ek = ExtKeyPair()
ek.set_seed(i2b(getSecretInt()))
ek_encoded: str = encode_secret_extkey(0x04358394, ek.encode_v())
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
rv = rpc(
"importdescriptors",
[
[
{
"desc": desc_external,
"timestamp": "now",
"active": True,
"range": [0, 10],
"next_index": 0,
},
{
"desc": desc_internal,
"timestamp": "now",
"active": True,
"internal": True,
},
],
],
wallet_override="descr_wallet",
)
logging.info(f"importdescriptors {rv}")
addr = rpc(
"getnewaddress", ["test descriptors"], wallet_override="descr_wallet"
)
addr_info = rpc(
"getaddressinfo",
[
addr,
],
wallet_override="descr_wallet",
)
assert addr_info["hdmasterfingerprint"] != "a55b7ea9"
rv = rpc("getwalletinfo", [], wallet_override="descr_wallet")
logging.info(f"getwalletinfo {rv}")
rpc(
"unloadwallet",
[
"descr_wallet",
],
)
bkp_path = os.path.join(walletdir, "wallet.dat" + ".bkp")
for i in range(1000):
if os.path.exists(bkp_path):
bkp_path = os.path.join(walletdir, "wallet.dat" + f".bkp{i}")
assert os.path.exists(bkp_path) is False
if os.path.isfile(walletpath):
shutil.copy(walletpath, bkp_path)
else:
shutil.copytree(walletpath, bkp_path)
with sqlite3.connect(walletpath) as conn:
c = conn.cursor()
c.executemany(
"UPDATE main SET value = :v WHERE key = :k", orig_active_descriptors
)
conn.commit()
rpc(
"loadwallet",
[
"descr_wallet",
],
)
addr = rpc("getnewaddress", wallet_override="descr_wallet")
addr_info = rpc(
"getaddressinfo",
[
addr,
],
wallet_override="descr_wallet",
)
logging.info(f"getaddressinfo {addr_info}")
assert addr_info["hdmasterfingerprint"] == "a55b7ea9"
finally:
rpc("stop")
bitcoind_process.wait(timeout=30)
if __name__ == "__main__":
unittest.main()