Browse Source

add new tests for Taproot/FROST

add_frost_channel_encryption
zebra-lucky 5 months ago
parent
commit
faabb25ea5
  1. 95
      conftest.py
  2. 8
      docs/frost-wallet-dev.md
  3. 2
      scripts/joinmarket-qt.py
  4. 3
      scripts/tumbler.py
  5. 5
      src/jmclient/__init__.py
  6. 41
      src/jmclient/configure.py
  7. 3
      src/jmclient/cryptoengine.py
  8. 11
      src/jmclient/frost_clients.py
  9. 26
      src/jmclient/frost_ipc.py
  10. 66
      src/jmclient/wallet.py
  11. 7
      src/jmclient/wallet_rpc.py
  12. 9
      test/jmclient/test_configure.py
  13. 964
      test/jmclient/test_frost_clients.py
  14. 341
      test/jmclient/test_frost_ipc.py
  15. 410
      test/jmclient/test_frost_wallet.py
  16. 1134
      test/jmclient/test_taproot_wallet.py
  17. 21
      test/jmclient/test_wallet.py
  18. 0
      test/jmfrost/chilldkg_example.py
  19. 0
      test/jmfrost/test_chilldkg_ref.py
  20. 18
      test/jmfrost/test_frost_ref.py
  21. 152
      test/jmfrost/trusted_keygen.py
  22. 154
      test/regtest_frost_joinmarket.cfg
  23. 150
      test/regtest_taproot_joinmarket.cfg

95
conftest.py

@ -172,3 +172,98 @@ def setup_regtest_bitcoind(pytestconfig):
local_command(stop_cmd)
# note, it is better to clean out ~/.bitcoin/regtest but too
# dangerous to automate it here perhaps
@pytest.fixture(scope="session")
def setup_regtest_taproot_bitcoind(pytestconfig):
"""
Setup regtest bitcoind and handle its clean up.
"""
conf = pytestconfig.getoption("--btcconf")
rpcuser = pytestconfig.getoption("--btcuser")
rpcpassword = pytestconfig.getoption("--btcpwd")
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 -txindex -conf={conf}'
stop_cmd = f'{bitcoincli_path} -regtest -rpcuser={rpcuser} -rpcpassword={rpcpassword} stop'
# determine bitcoind version
try:
bitcoind_version = get_bitcoind_version(bitcoind_path, conf)
except RuntimeError as exc:
pytest.exit(f"Cannot setup tests, bitcoind failing.\n{exc}")
if bitcoind_version[0] >= 26:
start_cmd += ' -allowignoredconf=1'
local_command(start_cmd, bg=True)
root_cmd = f'{bitcoincli_path} -regtest -rpcuser={rpcuser} -rpcpassword={rpcpassword}'
wallet_name = 'jm-test-taproot-wallet'
# Bitcoin Core v0.21+ does not create default wallet
# From Bitcoin Core 0.21.0 there is support for descriptor wallets, which
# are default from 23.x+ (including 22.99.0 development versions).
# We don't support descriptor wallets yet.
if bitcoind_version[0] >= 27:
create_wallet = (f'{root_cmd} -rpcwait -named createwallet '
f'wallet_name={wallet_name} descriptors=true')
else:
pytest.exit("Cannot setup tests, bitcoind version "
"must be 27 or greater.\n")
local_command(create_wallet)
local_command(f'{root_cmd} loadwallet {wallet_name}')
for i in range(2):
cpe = local_command(f'{root_cmd} -rpcwallet={wallet_name} getnewaddress')
if cpe.returncode != 0:
pytest.exit(f"Cannot setup tests, bitcoin-cli failing.\n{cpe.stdout.decode('utf-8')}")
destn_addr = cpe.stdout[:-1].decode('utf-8')
local_command(f'{root_cmd} -rpcwallet={wallet_name} generatetoaddress 301 {destn_addr}')
sleep(1)
yield
# shut down bitcoind
local_command(stop_cmd)
# note, it is better to clean out ~/.bitcoin/regtest but too
# dangerous to automate it here perhaps
@pytest.fixture(scope="session")
def setup_regtest_frost_bitcoind(pytestconfig):
"""
Setup regtest bitcoind and handle its clean up.
"""
conf = pytestconfig.getoption("--btcconf")
rpcuser = pytestconfig.getoption("--btcuser")
rpcpassword = pytestconfig.getoption("--btcpwd")
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 -txindex -conf={conf}'
stop_cmd = f'{bitcoincli_path} -regtest -rpcuser={rpcuser} -rpcpassword={rpcpassword} stop'
# determine bitcoind version
try:
bitcoind_version = get_bitcoind_version(bitcoind_path, conf)
except RuntimeError as exc:
pytest.exit(f"Cannot setup tests, bitcoind failing.\n{exc}")
if bitcoind_version[0] >= 26:
start_cmd += ' -allowignoredconf=1'
local_command(start_cmd, bg=True)
root_cmd = f'{bitcoincli_path} -regtest -rpcuser={rpcuser} -rpcpassword={rpcpassword}'
wallet_name = 'jm-test-frost-wallet'
# Bitcoin Core v0.21+ does not create default wallet
# From Bitcoin Core 0.21.0 there is support for descriptor wallets, which
# are default from 23.x+ (including 22.99.0 development versions).
# We don't support descriptor wallets yet.
if bitcoind_version[0] >= 27:
create_wallet = (f'{root_cmd} -rpcwait -named createwallet '
f'wallet_name={wallet_name} descriptors=true')
else:
pytest.exit("Cannot setup tests, bitcoind version "
"must be 27 or greater.\n")
local_command(create_wallet)
local_command(f'{root_cmd} loadwallet {wallet_name} true true')
yield
# shut down bitcoind
local_command(stop_cmd)
# note, it is better to clean out ~/.bitcoin/regtest but too
# dangerous to automate it here perhaps

8
docs/frost-wallet-dev.md

@ -13,6 +13,14 @@ usage.
Usual wallet usage interact with FROST/DKG functionality via IPC code in
`frost_ipc.py` (currently `AF_UNIX` socket for simplicity).
`jmclient.wallet_utils.open_wallet` has two new parameters:
- `load_dkg=False`: by default do not load `DKGStorage`
- `dkg_read_only=True`: load `DKGStorage` for read only commands
Additionally `open_wallet` params `read_only` and `dkg_read_only` can not
be mutually unset by design.
## Structure of DKG data in the DKGStorage
```

2
scripts/joinmarket-qt.py

@ -2462,7 +2462,7 @@ if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
#trigger start with a fake tx
jm_single().bc_interface.pushtx(b"\x00"*20)
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs")
logsdir = os.path.join(jm_single().datadir, "logs")
#tumble log will not always be used, but is made available anyway:
tumble_log = get_tumble_log(logsdir)
#ignored makers list persisted across entire app run

3
scripts/tumbler.py

@ -33,8 +33,7 @@ async def main():
jmprint('Error: Needs a wallet file', "error")
sys.exit(EXIT_ARGERROR)
load_program_config(config_path=options['datadir'])
logsdir = os.path.join(os.path.dirname(
jm_single().config_location), "logs")
logsdir = os.path.join(jm_single().datadir, "logs")
tumble_log = get_tumble_log(logsdir)
if jm_single().bc_interface is None:

5
src/jmclient/__init__.py

@ -26,7 +26,8 @@ from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportW
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin,
FidelityBondWatchonlyWallet, SegwitWalletFidelityBonds,
UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime,
UnknownAddressForLabel, TaprootWallet, FrostWallet)
UnknownAddressForLabel, TaprootWallet, FrostWallet,
TaprootWalletFidelityBonds, DKGManager)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage,
DKGStorage, DKGRecoveryStorage)
@ -83,7 +84,7 @@ from .websocketserver import JmwalletdWebSocketServerFactory, \
from .wallet_rpc import JMWalletDaemon
from .bond_calc import get_bond_values
from .frost_clients import FROSTClient
from .frost_ipc import FrostIPCClient
from .frost_ipc import FrostIPCServer, FrostIPCClient
# Set default logging handler to avoid "No handler found" warnings.
try:

41
src/jmclient/configure.py

@ -46,8 +46,7 @@ class AttributeDict(object):
logFormatter = logging.Formatter(
('%(asctime)s [%(threadName)-12.12s] '
'[%(levelname)-5.5s] %(message)s'))
logsdir = os.path.join(os.path.dirname(
global_singleton.config_location), "logs")
logsdir = os.path.join(global_singleton.datadir, "logs")
fileHandler = logging.FileHandler(
logsdir + '/{}.log'.format(value))
fileHandler.setFormatter(logFormatter)
@ -77,7 +76,7 @@ global_singleton.joinmarket_alert = joinmarket_alert
global_singleton.debug_silence = debug_silence
global_singleton.config = ConfigParser(strict=False)
#This is reset to a full path after load_program_config call
global_singleton.config_location = 'joinmarket.cfg'
global_singleton.config_fname = 'joinmarket.cfg'
#as above
global_singleton.commit_file_location = 'cmtdata/commitments.json'
global_singleton.wait_for_commitments = 0
@ -698,13 +697,12 @@ def load_program_config(config_path: str = "", bs: Optional[str] = None,
os.makedirs(os.path.join(global_singleton.datadir, "logs"))
if not os.path.exists(os.path.join(global_singleton.datadir, "cmtdata")):
os.makedirs(os.path.join(global_singleton.datadir, "cmtdata"))
global_singleton.config_location = os.path.join(
global_singleton.datadir, global_singleton.config_location)
config_location = os.path.join(
global_singleton.datadir, global_singleton.config_fname)
_remove_unwanted_default_settings(global_singleton.config)
try:
loadedFiles = global_singleton.config.read(
[global_singleton.config_location])
loadedFiles = global_singleton.config.read([config_location])
except UnicodeDecodeError:
jmprint("Error loading `joinmarket.cfg`, invalid file format.",
"info")
@ -717,7 +715,7 @@ def load_program_config(config_path: str = "", bs: Optional[str] = None,
global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
# Create default config file if not found
if len(loadedFiles) != 1:
with open(global_singleton.config_location, "w") as configfile:
with open(config_location, "w") as configfile:
configfile.write(defaultconfig)
jmprint("Created a new `joinmarket.cfg`. Please review and adopt the "
"settings and restart joinmarket.", "info")
@ -787,8 +785,7 @@ def load_program_config(config_path: str = "", bs: Optional[str] = None,
# and setting that in the plugin object; the plugin
# itself will switch on its own logging when ready,
# attaching a filehandler to the global log.
plogsdir = os.path.join(os.path.dirname(
global_singleton.config_location), "logs", p.name)
plogsdir = os.path.join(global_singleton.datadir, "logs", p.name)
if not os.path.exists(plogsdir):
os.makedirs(plogsdir)
p.set_log_dir(plogsdir)
@ -851,7 +848,8 @@ def _get_bitcoin_rpc_credentials(_config: ConfigParser) -> Tuple[str, str]:
raise ValueError("Invalid RPC auth credentials `rpc_user` and `rpc_password`")
return rpc_user, rpc_password
def get_blockchain_interface_instance(_config: ConfigParser):
def get_blockchain_interface_instance(_config: ConfigParser, *,
rpc_wallet_name=None):
# todo: refactor joinmarket module to get rid of loops
# importing here is necessary to avoid import loops
from jmclient.blockchaininterface import BitcoinCoreInterface, \
@ -876,8 +874,21 @@ def get_blockchain_interface_instance(_config: ConfigParser):
else:
raise ValueError('wrong network configured: ' + network)
rpc_user, rpc_password = _get_bitcoin_rpc_credentials(_config)
rpc_wallet_file = _config.get("BLOCKCHAIN", "rpc_wallet_file")
if rpc_wallet_name is not None:
rpc_wallet_file = rpc_wallet_name
else:
rpc_wallet_file = _config.get("BLOCKCHAIN", "rpc_wallet_file")
rpc = JsonRpc(rpc_host, rpc_port, rpc_user, rpc_password)
# code for TaprootWallet testing
if rpc_wallet_name and source in ['bitcoin-rpc', 'regtest',
'bitcoin-rpc-no-history']:
# create wallet with disable_private_keys=True, blank=True
rpc.call('createwallet', [rpc_wallet_name, True, True])
loaded_wallets = rpc.call("listwallets", [])
if not rpc_wallet_name in loaded_wallets:
log.info(f"Loading Bitcoin RPC wallet {rpc_wallet_name }...")
rpc.call('loadwallet', [rpc_wallet_name])
log.info("Done.")
if source == 'bitcoin-rpc': #pragma: no cover
bc_interface = BitcoinCoreInterface(rpc, network,
rpc_wallet_file)
@ -933,7 +944,9 @@ def update_persist_config(section: str, name: str, value: Any) -> bool:
sectionname = None
newlines = []
match_found = False
with open(jm_single().config_location, "r") as f:
config_location = os.path.join(jm_single().datadir,
jm_single().config_fname)
with open(config_location, "r") as f:
for line in f.readlines():
newline = line
# ignore comment lines
@ -956,7 +969,7 @@ def update_persist_config(section: str, name: str, value: Any) -> bool:
return False
# success: update in-mem and re-persist
jm_single().config.set(section, name, value)
with open(jm_single().config_location, "wb") as f:
with open(config_location, "wb") as f:
f.writelines([x.encode("utf-8") for x in newlines])
return True

3
src/jmclient/cryptoengine.py

@ -19,7 +19,7 @@ from .configure import get_network, jm_single
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, TYPE_P2TR_FROST = range(12)
TYPE_P2TR, TYPE_P2TR_FROST, TYPE_TAPROOT_WALLET_FIDELITY_BONDS = range(13)
NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET,
'signet': NET_SIGNET}
@ -532,4 +532,5 @@ ENGINES = {
TYPE_SEGWIT_WALLET_FIDELITY_BONDS: BTC_P2WPKH,
TYPE_P2TR: BTC_P2TR,
TYPE_P2TR_FROST: BTC_P2TR_FROST,
TYPE_TAPROOT_WALLET_FIDELITY_BONDS: BTC_P2TR,
}

11
src/jmclient/frost_clients.py

@ -451,8 +451,11 @@ class DKGClient:
if ready_list and len(ready_list) == len(self.hostpubkeys) - 1:
ext_recovery = coordinator.ext_recovery
self.finalize(session_id, coordinator.cmsg2, ext_recovery)
return True
return False
except Exception as e:
jlog.error(f'on_dkg_finalized: {repr(e)}')
return False
async def wait_on_dkg_output(self, session_id):
try:
@ -733,7 +736,8 @@ class FROSTClient(DKGClient):
if p == hostpubkey:
self.my_id = (i+1).to_bytes(32, 'big')
break
assert self.my_id is not None
assert self.my_id is not None, (f'unknown hostpubkey '
f'{hostpubkey.hex()}')
hostpubkeyhash = sha256(hostpubkey).digest()
session_id = sha256(os.urandom(32)).digest()
coordinator = FROSTCoordinator(session_id=session_id,
@ -822,7 +826,7 @@ class FROSTClient(DKGClient):
return None, None, None, None, None
pubkey = self.find_pubkey_by_pubkeyhash(pubkeyhash)
if not pubkey:
raise Exception(f'pubkey for {pubkeyhash.hex()} not found')
raise Exception(f'pubkey for {pubkeyhash} not found')
xpubkey = XOnlyPubKey(pubkey[1:])
if not xpubkey.verify_schnorr(session_id, hextobin(sig)):
raise Exception(f'signature verification failed')
@ -905,9 +909,6 @@ class FROSTClient(DKGClient):
raise Exception(f'secshare not found for '
f'{dkg_session_id.hex()}')
_pubshares = dkg._dkg_pubshares.get(dkg_session_id)
if not _pubshares:
raise Exception(f'pubshares not found for '
f'{dkg_session_id.hex()}')
pubshares = []
for i, pubshare in enumerate(_pubshares):
if (i+1) not in ids:

26
src/jmclient/frost_ipc.py

@ -100,7 +100,10 @@ class FrostIPCServer(IPCBase):
new_pubkey = dkg.find_dkg_pubkey(mixdepth, address_type, index)
if new_pubkey:
await self.send_dkg_pubkey(msg_id, new_pubkey)
else:
raise Exception('No pubkey found or generated')
except Exception as e:
await self.send_dkg_pubkey(msg_id, None)
jlog.error(f'FrostIPCServer.on_get_dkg_pubkey: {repr(e)}')
async def send_dkg_pubkey(self, msg_id, pubkey):
@ -129,6 +132,7 @@ class FrostIPCServer(IPCBase):
await self.send_frost_sig(msg_id, sig, pubkey, tweaked_pubkey)
except Exception as e:
jlog.error(f'FrostIPCServer.on_frost_sign: {repr(e)}')
await self.send_frost_sig(msg_id, None, None, None)
async def send_frost_sig(self, msg_id, sig, pubkey, tweaked_pubkey):
try:
@ -215,7 +219,15 @@ class FrostIPCClient(IPCBase):
self.msg_futures[self.msg_id] = fut
await fut
pubkey = fut.result()
jlog.debug('FrostIPCClient.get_dkg_pubkey successfully got pubkey')
if pubkey is None:
jlog.error(
f'FrostIPCClient.get_dkg_pubkey got None pubkey from '
f'FrostIPCServer for mixdepth={mixdepth}, '
f'address_type={address_type}, index={index}')
return pubkey
jlog.debug(f'FrostIPCClient.get_dkg_pubkey successfully got '
f'pubkey for mixdepth={mixdepth}, '
f'address_type={address_type}, index={index}')
return pubkey
except Exception as e:
jlog.error(f'FrostIPCClient.get_dkg_pubkey: {repr(e)}')
@ -237,7 +249,17 @@ class FrostIPCClient(IPCBase):
self.msg_futures[self.msg_id] = fut
await fut
sig, pubkey, tweaked_pubkey = fut.result()
jlog.debug('FrostIPCClient.frost_sign successfully got signature')
if sig is None:
jlog.error(
f'FrostIPCClient.frost_sign got None sig value from '
f'FrostIPCServer for mixdepth={mixdepth}, '
f'address_type={address_type}, index={index}, '
f'sighash={sighash.hex()}')
return sig, pubkey, tweaked_pubkey
jlog.debug(
f'FrostIPCClient.frost_sign successfully got signature '
f'for mixdepth={mixdepth}, address_type={address_type}, '
f'index={index}, sighash={sighash.hex()}')
return sig, pubkey, tweaked_pubkey
except Exception as e:
jlog.error(f'FrostIPCClient.frost_sign: {repr(e)}')

66
src/jmclient/wallet.py

@ -30,7 +30,7 @@ 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, TYPE_P2TR_FROST, ENGINES, \
detect_script_type, EngineError
detect_script_type, EngineError, TYPE_TAPROOT_WALLET_FIDELITY_BONDS
from .storage import DKGRecoveryStorage
from .support import get_random_bytes
from . import mn_encode, mn_decode
@ -526,7 +526,7 @@ class DKGManager:
return self._dkg_pubkey.get(session)
def add_party_data(self, *, session_id, dkg_output, hostpubkeys, t,
recovery_data, ext_recovery):
recovery_data, ext_recovery, save_dkg=True):
assert isinstance(dkg_output, tuple)
assert isinstance(dkg_output.secshare, bytes)
assert len(dkg_output.secshare) == 32
@ -547,12 +547,13 @@ class DKGManager:
self._dkg_t[session_id] = t
recovery_dkg = self.recovery_storage.data[self.RECOVERY_STORAGE_KEY]
recovery_dkg[session_id] = (ext_recovery, recovery_data)
recovery_dkg[session_id] = [ext_recovery, recovery_data]
self.save()
if save_dkg:
self.save()
def add_coordinator_data(self, *, session_id, dkg_output, hostpubkeys, t,
recovery_data, ext_recovery):
recovery_data, ext_recovery, save_dkg=True):
assert isinstance(dkg_output, tuple)
assert isinstance(dkg_output.secshare, bytes)
assert len(dkg_output.secshare) == 32
@ -583,9 +584,10 @@ class DKGManager:
self._dkg_sessions[md_type_idx] = session_id
recovery_dkg = self.recovery_storage.data[self.RECOVERY_STORAGE_KEY]
recovery_dkg[session_id] = (ext_recovery, recovery_data)
recovery_dkg[session_id] = [ext_recovery, recovery_data]
self.save()
if save_dkg:
self.save()
async def dkg_recover(self, dkgrec_path):
rec_storage = DKGRecoveryStorage(
@ -660,14 +662,13 @@ class DKGManager:
self._dkg_pubkey.pop(c, None)
self._dkg_hostpubkeys.pop(c, None)
self._dkg_t.pop(c, None)
self.save()
for md_type_idx in list(self._dkg_sessions.keys()):
if self._dkg_sessions[md_type_idx] == c:
self._dkg_sessions.pop(md_type_idx)
if c in self._dkg_secshare:
res += f'dkg data for session {sess_id} deleted\n'
else:
res +=f'not found dkg data for session {sess_id}\n'
for md_type_idx in list(self._dkg_sessions.keys()):
if self._dkg_sessions[md_type_idx] == c:
res += f'session data for session {sess_id} deleted\n'
self._dkg_sessions.pop(md_type_idx)
self.save()
return res
except Exception as e:
@ -706,23 +707,22 @@ class DKGManager:
f'\nNot decrypted sesions:\n{json.dumps(enc_res, indent=4)}')
def recdkg_rm(self, session_ids: list):
res = ''
rm_sess_ids = []
not_found_ids = []
recovery_dkg = self.recovery_storage.data[self.RECOVERY_STORAGE_KEY]
for session_id, (ext_recovery, recovery_data) in recovery_dkg.items():
if session_id in session_ids:
rm_sess_ids.append(session_id)
else:
not_found_ids.append(session_id)
for session_id in rm_sess_ids:
del recovery_dkg[session_id]
res += (f'dkg recovery data for session {session_id.hex()}'
f' deleted\n')
for session_id in not_found_ids:
res += f'not found dkg data for session {session_id.hex()}\n'
self.save()
return res
try:
res = ''
rec_dkg = self.recovery_storage.data[self.RECOVERY_STORAGE_KEY]
for sess_id in session_ids:
c = hextobin(sess_id)
if c in rec_dkg.keys():
rec_dkg.pop(c, None)
res += (f'dkg recovery data for session {sess_id}'
f' deleted\n')
else:
res += (f'not found dkg recovery data for session'
f' {sess_id}\n')
self.save()
return res
except Exception as e:
jmprint(f'error: {repr(e)}', 'error')
class BaseWallet(object):
@ -870,7 +870,8 @@ class BaseWallet(object):
return 'p2pkh'
elif self.TYPE == TYPE_P2SH_P2WPKH:
return 'p2sh-p2wpkh'
elif self.TYPE in (TYPE_P2TR, TYPE_P2TR_FROST):
elif self.TYPE in (TYPE_P2TR, TYPE_TAPROOT_WALLET_FIDELITY_BONDS,
TYPE_P2TR_FROST):
return 'p2tr'
elif self.TYPE in (TYPE_P2WPKH,
TYPE_SEGWIT_WALLET_FIDELITY_BONDS):
@ -3305,9 +3306,12 @@ class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, SNICKER
class SegwitWalletFidelityBonds(FidelityBondMixin, SegwitWallet):
TYPE = TYPE_SEGWIT_WALLET_FIDELITY_BONDS
class TaprootWallet(BIP39WalletMixin, BIP86Wallet):
class TaprootWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, SNICKERWalletMixin, BIP86Wallet):
TYPE = TYPE_P2TR
class TaprootWalletFidelityBonds(FidelityBondMixin, TaprootWallet):
TYPE = TYPE_TAPROOT_WALLET_FIDELITY_BONDS
class BIP32FrostMixin(BaseWallet):

7
src/jmclient/wallet_rpc.py

@ -526,7 +526,7 @@ class JMWalletDaemon(Service):
# We're running the tumbler.
assert self.tumble_log is not None
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs")
logsdir = os.path.join(jm_single().datadir, "logs")
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile'])
tumbler_taker_finished_update(self.taker, sfile, self.tumble_log, self.tumbler_options, res, fromtx, waittime, txdetails)
@ -1467,8 +1467,7 @@ class JMWalletDaemon(Service):
except ScheduleGenerationErrorNoFunds:
raise NotEnoughCoinsForTumbler()
logsdir = os.path.join(os.path.dirname(jm_single().config_location),
"logs")
logsdir = os.path.join(jm_single().datadir, "logs")
sfile = os.path.join(logsdir, tumbler_options['schedulefile'])
with open(sfile, "wb") as f:
f.write(schedule_to_text(schedule))
@ -1521,7 +1520,7 @@ class JMWalletDaemon(Service):
if not self.tumbler_options or not self.coinjoin_state == CJ_TAKER_RUNNING:
return make_jmwalletd_response(request, status=404)
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs")
logsdir = os.path.join(jm_single().datadir, "logs")
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile'])
res, schedule = get_schedule(sfile)

9
test/jmclient/test_configure.py

@ -16,15 +16,6 @@ def test_attribute_dict():
assert ad["foo"] == 1
def test_load_config(tmpdir):
load_test_config(bs="regtest")
jm_single().config_location = "joinmarket.cfg"
with pytest.raises(SystemExit):
load_test_config(config_path=str(tmpdir), bs="regtest")
jm_single().config_location = "joinmarket.cfg"
load_test_config()
def test_blockchain_sources():
load_test_config()
for src in ["dummy"]:

964
test/jmclient/test_frost_clients.py

@ -0,0 +1,964 @@
# -*- coding: utf-8 -*-
from pprint import pprint
from hashlib import sha256
from unittest import IsolatedAsyncioTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
import pytest
import jmbitcoin as btc
from jmbase import get_log
from jmclient import (
load_test_config, jm_single, get_network, cryptoengine, VolatileStorage,
FrostWallet, WalletService)
from jmclient.frost_clients import DKGClient, FROSTClient
from jmfrost.chilldkg_ref.chilldkg import (
hostpubkey_gen, ParticipantMsg1, CoordinatorMsg1, ParticipantMsg2,
CoordinatorMsg2)
pytestmark = pytest.mark.usefixtures("setup_regtest_frost_bitcoind")
log = get_log()
async def get_populated_wallet(entropy=None):
storage = VolatileStorage()
dkg_storage = VolatileStorage()
recovery_storage = VolatileStorage()
FrostWallet.initialize(storage, dkg_storage, recovery_storage,
get_network(), entropy=entropy)
wlt = FrostWallet(storage, dkg_storage, recovery_storage)
await wlt.async_init(storage)
return wlt
async def populate_dkg_session(test_case):
dkgc1 = DKGClient(test_case.wlt_svc1)
dkgc2 = DKGClient(test_case.wlt_svc2)
dkgc3 = DKGClient(test_case.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
test_case.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
test_case.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
test_case.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
test_case.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1)
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2)
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1)
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
test_case.nick2, session_id, pmsg2_2)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
test_case.nick3, session_id, pmsg2_3)
cmsg2 = dkgc3.deserialize_cmsg2(cmsg2)
assert dkgc2.finalize(session_id, cmsg2, ext_recovery)
assert dkgc3.finalize(session_id, cmsg2, ext_recovery)
dkgc1.on_dkg_finalized(test_case.nick2, session_id)
dkgc1.on_dkg_finalized(test_case.nick3, session_id)
return session_id
class DKGClientTestCaseBase(IsolatedAsyncioTestCase):
def setUp(self):
load_test_config(config_path='./test_frost')
btc.select_chain_params("bitcoin/regtest")
cryptoengine.BTC_P2TR.VBYTE = 100
jm_single().bc_interface.tick_forward_chain_interval = 2
async def asyncSetUp(self):
entropy1 = bytes.fromhex('8e5e5677fb302874a607b63ad03ba434')
entropy2 = bytes.fromhex('38dfa80fbb21b32b2b2740e00a47de9d')
entropy3 = bytes.fromhex('3ad9c77fcd1d537b6ef396952d1221a0')
# entropy4 wor wallet with hospubkey not in joinmarket.cfg
entropy4 = bytes.fromhex('ce88b87f6c85d651e416b8173ab95e57')
self.wlt1 = await get_populated_wallet(entropy1)
self.hostpubkey1 = hostpubkey_gen(self.wlt1._hostseckey[:32])
self.wlt_svc1 = WalletService(self.wlt1)
self.wlt2 = await get_populated_wallet(entropy2)
self.hostpubkey2 = hostpubkey_gen(self.wlt2._hostseckey[:32])
self.wlt_svc2 = WalletService(self.wlt2)
self.wlt3 = await get_populated_wallet(entropy3)
self.hostpubkey3 = hostpubkey_gen(self.wlt3._hostseckey[:32])
self.wlt_svc3 = WalletService(self.wlt3)
self.wlt4= await get_populated_wallet(entropy4)
self.hostpubkey4 = hostpubkey_gen(self.wlt4._hostseckey[:32])
self.wlt_svc4 = WalletService(self.wlt4)
self.nick1, self.nick2, self.nick3, self.nick4 = [
'nick1', 'nick2', 'nick3', 'nick4'
]
class DKGClientTestCase(DKGClientTestCaseBase):
async def test_dkg_init(self):
# test wallet with unknown hostpubkey
dkgc1 = DKGClient(self.wlt_svc4)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
assert hostpubkeyhash_hex is None
assert session_id is None
assert sig_hex is None
dkgc1 = DKGClient(self.wlt_svc1)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
assert hostpubkeyhash_hex and len(hostpubkeyhash_hex) == 64
assert session_id and len(session_id) == 32
assert sig_hex and len(sig_hex) == 128
async def test_on_dkg_init(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
# fail with wrong pubkeyhash
hostpubkeyhash4_hex = sha256(self.hostpubkey4).digest()
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash4_hex, session_id, sig_hex)
for v in [nick1, hostpubkeyhash2_hex, session_id2_hex,
sig2_hex, pmsg1]:
assert v is None
# fail with wrong sig
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, '01020304'*16)
for v in [nick1, hostpubkeyhash2_hex, session_id2_hex,
sig2_hex, pmsg1]:
assert v is None
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
assert nick1 == self.nick1
assert hostpubkeyhash2_hex and len(hostpubkeyhash2_hex) == 64
assert session_id2_hex and len(session_id2_hex) == 64
assert bytes.fromhex(session_id2_hex) == session_id
assert sig_hex and len(sig_hex) == 128
assert pmsg1 is not None
# fail on second call with right params
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
for v in [nick1, hostpubkeyhash2_hex, session_id2_hex,
sig2_hex, pmsg1]:
assert v is None
async def test_party_step1(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
# fail with unknown session_id
pmsg1 = dkgc2.party_step1(b'\x05'*32)
assert pmsg1 is None
# fail when session.state1 aleready set
pmsg1 = dkgc2.party_step1(session_id)
assert pmsg1 is None
session = dkgc2.dkg_sessions.get(session_id)
session.state1 = None
pmsg1 = dkgc2.party_step1(session_id)
assert pmsg1 is not None
assert isinstance(pmsg1, bytes)
session.state1 = None
pmsg1 = dkgc2.party_step1(session_id, serialize=False)
assert pmsg1 is not None
assert isinstance(pmsg1, ParticipantMsg1)
def test_on_dkg_pmsg1(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
# party2 added pmsg1, no ready_list, no cmsg1 returned yet
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
assert ready_list is None
assert cmsg1 is None
# unknown coordinator session
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, b'\xaa'*32, sig3_hex, pmsg1_3)
assert ready_list is None
assert cmsg1 is None
# unknown pubkeyhash
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, b'\xaa'*32, session_id, sig3_hex, pmsg1_3)
assert ready_list is None
assert cmsg1 is None
# wrong sig
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, 'aa'*64, pmsg1_3)
assert ready_list is None
assert cmsg1 is None
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
assert ready_list == set([self.nick2, self.nick3])
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
assert isinstance(cmsg1, CoordinatorMsg1)
def test_coordinator_step1(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
# unknown session_id
cmsg1 = dkgc1.coordinator_step1(b'\xaa'*32)
assert cmsg1 is None
# coordinator.state already set
cmsg1 = dkgc1.coordinator_step1(session_id)
assert cmsg1 is None
coordinator = dkgc1.dkg_coordinators.get(session_id)
coordinator.state = None
cmsg1 = dkgc1.coordinator_step1(session_id)
def test_party_step2(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
# unknown session_id
pmsg2 = dkgc2.party_step2(b'\xaa'*32, cmsg1)
assert pmsg2 is None
pmsg2 = dkgc2.party_step2(session_id, cmsg1)
assert cmsg1 is not None
pmsg2 = dkgc1.deserialize_pmsg2(pmsg2)
assert isinstance(pmsg2, ParticipantMsg2)
# session.state2 already set
pmsg2 = dkgc2.party_step2(session_id, cmsg1)
assert pmsg2 is None
def test_on_dkg_pmsg2(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1)
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2)
assert isinstance(pmsg2_2, ParticipantMsg2)
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1)
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3)
assert isinstance(pmsg2_3, ParticipantMsg2)
# party2 added pmsg2, no ready_list, no cmsg2 returned yet
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick2, session_id, pmsg2_2)
assert ready_list is None
assert cmsg2 is None
assert ext_recovery is None
# unknown coordinator session
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, b'\xaa'*32, pmsg2_3)
assert ready_list is None
assert cmsg2 is None
assert ext_recovery is None
# unknown party nick
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick4, session_id, pmsg2_3)
assert ready_list is None
assert cmsg2 is None
assert ext_recovery is None
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, session_id, pmsg2_3)
cmsg2 = dkgc1.deserialize_cmsg2(cmsg2)
assert ready_list == set([self.nick2, self.nick3])
assert isinstance(cmsg2, CoordinatorMsg2)
assert isinstance(ext_recovery, bytes)
# party pubkey for nick3 not found
coordinator = dkgc1.dkg_coordinators.get(session_id)
session3 = coordinator.sessions.pop(self.hostpubkey3)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, session_id, pmsg2_3)
assert ready_list is None
assert cmsg2 is None
assert ext_recovery is None
coordinator.sessions[self.hostpubkey3] = session3
# pmsg2 already set in coordinator sessions
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, session_id, pmsg2_3)
assert ready_list is None
assert cmsg2 is None
assert ext_recovery is None
def test_coordinator_step2(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1)
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2)
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1)
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick2, session_id, pmsg2_2)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, session_id, pmsg2_3)
# unknown session_id
cmsg2 = dkgc1.coordinator_step2(b'\xaa'*32)
assert cmsg2 is None
# coordinator.cmsg2 already set
cmsg2 = dkgc1.coordinator_step2(session_id)
assert cmsg2 is None
coordinator = dkgc1.dkg_coordinators.get(session_id)
coordinator.cmsg2 = None
cmsg2 = dkgc1.coordinator_step2(session_id)
cmsg2 = dkgc1.deserialize_cmsg2(cmsg2)
assert isinstance(cmsg2, CoordinatorMsg2)
def test_dkg_finalize(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1)
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2)
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1)
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick2, session_id, pmsg2_2)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, session_id, pmsg2_3)
cmsg2 = dkgc3.deserialize_cmsg2(cmsg2)
# unknown session_id
assert not dkgc2.finalize(b'\xaa'*32, cmsg2, ext_recovery)
assert dkgc2.finalize(session_id, cmsg2, ext_recovery)
assert dkgc3.finalize(session_id, cmsg2, ext_recovery)
# session.dkg_output already set
assert not dkgc2.finalize(session_id, cmsg2, ext_recovery)
assert not dkgc3.finalize(session_id, cmsg2, ext_recovery)
def test_on_dkg_finalized(self):
dkgc1 = DKGClient(self.wlt_svc1)
dkgc2 = DKGClient(self.wlt_svc2)
dkgc3 = DKGClient(self.wlt_svc3)
hostpubkeyhash_hex, session_id, sig_hex = dkgc1.dkg_init(0, 0, 0)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pmsg1_2
) = dkgc2.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_2 = dkgc2.deserialize_pmsg1(pmsg1_2)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pmsg1_3
) = dkgc3.on_dkg_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
pmsg1_3 = dkgc2.deserialize_pmsg1(pmsg1_3)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick2, hostpubkeyhash2_hex, session_id, sig2_hex, pmsg1_2)
ready_list, cmsg1 = dkgc1.on_dkg_pmsg1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pmsg1_3)
cmsg1 = dkgc1.deserialize_cmsg1(cmsg1)
pmsg2_2 = dkgc2.party_step2(session_id, cmsg1)
pmsg2_2 = dkgc2.deserialize_pmsg2(pmsg2_2)
pmsg2_3 = dkgc3.party_step2(session_id, cmsg1)
pmsg2_3 = dkgc3.deserialize_pmsg2(pmsg2_3)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick2, session_id, pmsg2_2)
ready_list, cmsg2, ext_recovery = dkgc1.on_dkg_pmsg2(
self.nick3, session_id, pmsg2_3)
cmsg2 = dkgc3.deserialize_cmsg2(cmsg2)
assert dkgc2.finalize(session_id, cmsg2, ext_recovery)
assert dkgc3.finalize(session_id, cmsg2, ext_recovery)
# unknown session_id
dkgc1.on_dkg_finalized(self.nick2, b'\xaa'*32)
assert not dkgc1.on_dkg_finalized(self.nick2, session_id)
assert dkgc1.on_dkg_finalized(self.nick3, session_id)
class FROSTClientTestCase(DKGClientTestCaseBase):
async def asyncSetUp(self):
await super().asyncSetUp()
self.dkg_session_id = await populate_dkg_session(self)
self.fc1 = FROSTClient(self.wlt_svc1)
self.fc2 = FROSTClient(self.wlt_svc2)
self.fc3 = FROSTClient(self.wlt_svc3)
self.fc4 = FROSTClient(self.wlt_svc4)
async def test_frost_init(self):
msg_bytes = bytes.fromhex('aabb'*16)
# test wallet with unknown hostpubkey
hostpubkeyhash_hex, session_id, sig_hex = self.fc4.frost_init(
self.dkg_session_id, msg_bytes)
assert hostpubkeyhash_hex is None
assert session_id is None
assert sig_hex is None
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
assert hostpubkeyhash_hex and len(hostpubkeyhash_hex) == 64
assert session_id and len(session_id) == 32
assert sig_hex and len(sig_hex) == 128
async def test_on_frost_init(self):
msg_bytes = bytes.fromhex('aabb'*16)
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
# fail with wrong pubkeyhash
hostpubkeyhash4_hex = sha256(self.hostpubkey4).digest()
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash4_hex, session_id, sig_hex)
for v in [nick1, hostpubkeyhash2_hex,
session_id2_hex, sig2_hex, pub_nonce]:
assert v is None
# fail with wrong sig
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, '01020304'*16)
for v in [nick1, hostpubkeyhash2_hex,
session_id2_hex, sig2_hex, pub_nonce]:
assert v is None
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
assert nick1 == self.nick1
assert hostpubkeyhash2_hex and len(hostpubkeyhash2_hex) == 64
assert session_id2_hex and len(session_id2_hex) == 64
assert bytes.fromhex(session_id2_hex) == session_id
assert sig_hex and len(sig_hex) == 128
assert pub_nonce and len(pub_nonce) == 66
# fail on second call with right params
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
for v in [nick1, hostpubkeyhash2_hex,
session_id2_hex, sig2_hex, pub_nonce]:
assert v is None
def test_frost_round1(self):
msg_bytes = bytes.fromhex('aabb'*16)
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
# fail with unknown session_id
pub_nonce = self.fc2.party_step1(b'\x05'*32)
assert pub_nonce is None
# fail with session.sec_nonce already set
pub_nonce = self.fc2.frost_round1(session_id)
assert pub_nonce is None
session = self.fc2.frost_sessions.get(session_id)
session.sec_nonce = None
pub_nonce = self.fc2.frost_round1(session_id)
assert pub_nonce and len(pub_nonce) == 66
def test_on_frost_round1(self):
msg_bytes = bytes.fromhex('aabb'*16)
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce2
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
(
nick1,
hostpubkeyhash3_hex,
session_id3_hex,
sig3_hex,
pub_nonce3
) = self.fc3.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
# unknown session_id
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, hostpubkeyhash2_hex, b'\xaa'*32,
sig2_hex, pub_nonce2)
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]:
assert v is None
# unknown pubkeyhash
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, 'bb'*32, session_id, sig2_hex, pub_nonce2)
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]:
assert v is None
# wrong sig
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, hostpubkeyhash2_hex, session_id, '1234'*32, pub_nonce2)
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]:
assert v is None
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, hostpubkeyhash2_hex, session_id,
sig2_hex, pub_nonce2)
assert ready_list == set([self.nick2])
assert nonce_agg and len(nonce_agg)== 66
assert dkg_session_id and dkg_session_id == self.dkg_session_id
assert ids == [1, 2]
assert msg and len(msg) == 32 and msg == msg_bytes
# miminum pub_nonce set already presented, ignoring additional
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick3, hostpubkeyhash3_hex, session_id, sig3_hex, pub_nonce3)
for v in [ready_list, nonce_agg, dkg_session_id, ids, msg]:
assert v is None
def test_frost_agg1(self):
msg_bytes = bytes.fromhex('aabb'*16)
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce2
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, hostpubkeyhash2_hex, session_id,
sig2_hex, pub_nonce2)
# fail on unknown session_id
(
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.frost_agg1(b'\xaa'*32)
for v in [nonce_agg, dkg_session_id, ids, msg]:
assert v is None
# fail with coordinator.nonce_agg already set
(
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.frost_agg1(session_id)
for v in [nonce_agg, dkg_session_id, ids, msg]:
assert v is None
coordinator = self.fc1.frost_coordinators.get(session_id)
coordinator.nonce_agg = None
(
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.frost_agg1(session_id)
assert nonce_agg and len(nonce_agg)== 66
assert dkg_session_id and dkg_session_id == self.dkg_session_id
assert ids == [1, 2]
assert msg and len(msg) == 32 and msg == msg_bytes
def test_frost_round2(self):
msg_bytes = bytes.fromhex('aabb'*16)
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce2
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, hostpubkeyhash2_hex, session_id,
sig2_hex, pub_nonce2)
# fail on unknown session_id
partial_sig = self.fc2.frost_round2(
b'\xaa'*32, nonce_agg, self.dkg_session_id, ids, msg)
# fail on unknown dkg_session_id
partial_sig = self.fc2.frost_round2(
session_id, nonce_agg, b'\xdd'*32, ids, msg)
partial_sig = self.fc2.frost_round2(
session_id, nonce_agg, self.dkg_session_id, ids, msg)
assert partial_sig and len(partial_sig) == 32
# session.partial_sig already set
partial_sig = self.fc2.frost_round2(
session_id, nonce_agg, self.dkg_session_id, ids, msg)
assert partial_sig is None
def test_on_frost_round2(self):
msg_bytes = bytes.fromhex('aabb'*16)
hostpubkeyhash_hex, session_id, sig_hex = self.fc1.frost_init(
self.dkg_session_id, msg_bytes)
(
nick1,
hostpubkeyhash2_hex,
session_id2_hex,
sig2_hex,
pub_nonce2
) = self.fc2.on_frost_init(
self.nick1, hostpubkeyhash_hex, session_id, sig_hex)
(
ready_list,
nonce_agg,
dkg_session_id,
ids,
msg
) = self.fc1.on_frost_round1(
self.nick2, hostpubkeyhash2_hex, session_id,
sig2_hex, pub_nonce2)
partial_sig = self.fc2.frost_round2(
session_id, nonce_agg, self.dkg_session_id, ids, msg)
# unknown party nick
sig = self.fc1.on_frost_round2(self.nick4, session_id, partial_sig)
assert sig is None
# party pubkey for nick3 not found
coordinator = self.fc1.frost_coordinators.get(session_id)
session2 = coordinator.sessions.pop(self.hostpubkey2)
sig = self.fc1.on_frost_round2(self.nick2, session_id, partial_sig)
assert sig is None
coordinator.sessions[self.hostpubkey2] = session2
# fail on unknown session_id
sig = self.fc1.on_frost_round2(self.nick2, b'\xaa'*32, partial_sig)
assert sig is None
sig = self.fc1.on_frost_round2(self.nick2, session_id, partial_sig)
assert sig and len(sig) == 64
# partial_sig already set in coordinator
sig = self.fc1.on_frost_round2(self.nick2, session_id, partial_sig)
assert sig is None

341
test/jmclient/test_frost_ipc.py

@ -0,0 +1,341 @@
# -*- coding: utf-8 -*-
import asyncio
import base64
import time
from pprint import pprint
from unittest import IsolatedAsyncioTestCase
import jmclient # install asyncioreactor
from twisted.internet import reactor
import pytest
import jmbitcoin as btc
from jmbase import get_log
from jmclient import (
load_test_config, jm_single, get_network, cryptoengine, VolatileStorage,
FrostWallet, WalletService)
from jmclient import FrostIPCServer, FrostIPCClient
from jmclient.frost_clients import FROSTClient
from test_frost_clients import populate_dkg_session
pytestmark = pytest.mark.usefixtures("setup_regtest_frost_bitcoind")
log = get_log()
async def get_populated_wallet(entropy=None):
storage = VolatileStorage()
dkg_storage = VolatileStorage()
recovery_storage = VolatileStorage()
FrostWallet.initialize(storage, dkg_storage, recovery_storage,
get_network(), entropy=entropy)
wlt = FrostWallet(storage, dkg_storage, recovery_storage)
await wlt.async_init(storage)
return wlt
class DummyFrostJMClientProtocol:
def __init__(self, factory, client, nick):
self.nick = nick
self.factory = factory
self.client = client
self.party_clients = {}
async def dkg_gen(self):
log.debug(f'Coordinator call dkg_gen')
client = self.factory.client
md_type_idx = None
session_id = None
session = None
while True:
if md_type_idx is None:
md_type_idx = await client.dkg_gen()
if md_type_idx is None:
log.debug('finished dkg_gen execution')
break
if session_id is None:
session_id, _, session = self.dkg_init(*md_type_idx)
if session_id is None:
log.warn('could not get session_id from dkg_init}')
await asyncio.sleep(5)
continue
pub = await client.wait_on_dkg_output(session_id)
if not pub:
session_id = None
session = None
continue
if session.dkg_output:
md_type_idx = None
session_id = None
session = None
client.dkg_gen_list.pop(0)
continue
def dkg_init(self, mixdepth, address_type, index):
log.debug(f'Coordinator call dkg_init '
f'({mixdepth}, {address_type}, {index})')
client = self.factory.client
hostpubkeyhash, session_id, sig = client.dkg_init(
mixdepth, address_type, index)
coordinator = client.dkg_coordinators.get(session_id)
session = client.dkg_sessions.get(session_id)
if session_id and session and coordinator:
session.dkg_init_sec = time.time()
for _, pc in self.party_clients.items():
async def on_dkg_init(pc, nick, hostpubkeyhash,
session_id, sig):
await pc.on_dkg_init(
nick, hostpubkeyhash, session_id, sig)
asyncio.create_task(on_dkg_init(
pc, self.nick, hostpubkeyhash, session_id, sig))
return session_id, coordinator, session
return None, None, None
async def on_dkg_init(self, nick, hostpubkeyhash, session_id, sig):
client = self.factory.client
nick, hostpubkeyhash, session_id, sig, pmsg1 = client.on_dkg_init(
nick, hostpubkeyhash, session_id, sig)
if pmsg1:
pc = self.party_clients[nick]
session_id = bytes.fromhex(session_id)
await pc.on_dkg_pmsg1(
self.nick, hostpubkeyhash, session_id, sig, pmsg1)
async def on_dkg_pmsg1(self, nick, hostpubkeyhash, session_id, sig, pmsg1):
client = self.factory.client
pmsg1 = client.deserialize_pmsg1(pmsg1)
ready_nicks, cmsg1 = client.on_dkg_pmsg1(
nick, hostpubkeyhash, session_id, sig, pmsg1)
if ready_nicks and cmsg1:
for party_nick in ready_nicks:
pc = self.party_clients[party_nick]
await pc.on_dkg_cmsg1(self.nick, session_id, cmsg1)
async def on_dkg_cmsg1(self, nick, session_id, cmsg1):
client = self.factory.client
session = client.dkg_sessions.get(session_id)
if not session:
log.error(f'on_dkg_cmsg1: session {session_id} not found')
return {'accepted': True}
if session and session.coord_nick == nick:
cmsg1 = client.deserialize_cmsg1(cmsg1)
pmsg2 = client.party_step2(session_id, cmsg1)
if pmsg2:
pc = self.party_clients[nick]
await pc.on_dkg_pmsg2(self.nick, session_id, pmsg2)
else:
log.error(f'on_dkg_cmsg1: not coordinator nick {nick}')
async def on_dkg_pmsg2(self, nick, session_id, pmsg2):
client = self.factory.client
pmsg2 = client.deserialize_pmsg2(pmsg2)
ready_nicks, cmsg2, ext_recovery = client.on_dkg_pmsg2(
nick, session_id, pmsg2)
if ready_nicks and cmsg2 and ext_recovery:
for party_nick in ready_nicks:
pc = self.party_clients[party_nick]
await pc.on_dkg_cmsg2(
self.nick, session_id, cmsg2, ext_recovery)
async def on_dkg_cmsg2(self, nick, session_id, cmsg2, ext_recovery):
client = self.factory.client
session = client.dkg_sessions.get(session_id)
if not session:
log.error(f'on_dkg_cmsg2: session {session_id} not found')
return {'accepted': True}
if session and session.coord_nick == nick:
cmsg2 = client.deserialize_cmsg2(cmsg2)
finalized = client.finalize(session_id, cmsg2, ext_recovery)
if finalized:
pc = self.party_clients[nick]
await pc.on_dkg_finalized(self.nick, session_id)
else:
log.error(f'on_dkg_cmsg2: not coordinator nick {nick}')
async def on_dkg_finalized(self, nick, session_id):
client = self.factory.client
log.debug(f'Coordinator get dkgfinalized')
client.on_dkg_finalized(nick, session_id)
def frost_init(self, dkg_session_id, msg_bytes):
log.debug(f'Coordinator call frost_init')
client = self.factory.client
hostpubkeyhash, session_id, sig = client.frost_init(
dkg_session_id, msg_bytes)
coordinator = client.frost_coordinators.get(session_id)
session = client.frost_sessions.get(session_id)
if session_id and session and coordinator:
coordinator.frost_init_sec = time.time()
for _, pc in self.party_clients.items():
async def on_frost_init(pc, nick, hostpubkeyhash,
session_id, sig):
await pc.on_frost_init(
nick, hostpubkeyhash, session_id, sig)
asyncio.create_task(on_frost_init(
pc, self.nick, hostpubkeyhash, session_id, sig))
return session_id, coordinator, session
async def on_frost_init(self, nick, hostpubkeyhash, session_id, sig):
client = self.factory.client
(
nick,
hostpubkeyhash,
session_id,
sig,
pub_nonce
) = client.on_frost_init(nick, hostpubkeyhash, session_id, sig)
if pub_nonce:
pc = self.party_clients[nick]
session_id = bytes.fromhex(session_id)
await pc.on_frost_round1(
self.nick, hostpubkeyhash, session_id, sig, pub_nonce)
async def on_frost_round1(self, nick, hostpubkeyhash,
session_id, sig, pub_nonce):
client = self.factory.client
(
ready_nicks,
nonce_agg,
dkg_session_id,
ids,
msg
) = client.on_frost_round1(
nick, hostpubkeyhash, session_id, sig, pub_nonce)
if ready_nicks and nonce_agg:
for party_nick in ready_nicks:
pc = self.party_clients[nick]
self.frost_agg1(pc, self.nick, session_id, nonce_agg,
dkg_session_id, ids, msg)
def frost_agg1(self, pc, nick, session_id,
nonce_agg, dkg_session_id, ids, msg):
pc.on_frost_agg1(
self.nick, session_id, nonce_agg, dkg_session_id, ids, msg)
def on_frost_agg1(self, nick, session_id,
nonce_agg, dkg_session_id, ids, msg):
client = self.factory.client
session = client.frost_sessions.get(session_id)
if not session:
log.error(f'on_frost_agg1: session {session_id} not found')
return
if session and session.coord_nick == nick:
partial_sig = client.frost_round2(
session_id, nonce_agg, dkg_session_id, ids, msg)
if partial_sig:
pc = self.party_clients[nick]
pc.on_frost_round2(self.nick, session_id, partial_sig)
else:
log.error(f'on_frost_agg1: not coordinator nick {nick}')
def on_frost_round2(self, nick, session_id, partial_sig):
client = self.factory.client
sig = client.on_frost_round2(nick, session_id, partial_sig)
if sig:
log.debug(f'Successfully get signature {sig.hex()[:8]}...')
class DummyFrostJMClientProtocolFactory:
protocol = DummyFrostJMClientProtocol
def __init__(self, client, nick):
self.client = client
self.proto_client = self.protocol(self, self.client, nick)
def add_party_client(self, nick, party_client):
self.proto_client.party_clients[nick] = party_client
def getClient(self):
return self.proto_client
class FrostIPCTestCaseBase(IsolatedAsyncioTestCase):
def setUp(self):
load_test_config(config_path='./test_frost')
btc.select_chain_params("bitcoin/regtest")
cryptoengine.BTC_P2TR.VBYTE = 100
jm_single().bc_interface.tick_forward_chain_interval = 2
async def asyncSetUp(self):
self.nick1, self.nick2, self.nick3 = ['nick1', 'nick2', 'nick3']
entropy1 = bytes.fromhex('8e5e5677fb302874a607b63ad03ba434')
entropy2 = bytes.fromhex('38dfa80fbb21b32b2b2740e00a47de9d')
entropy3 = bytes.fromhex('3ad9c77fcd1d537b6ef396952d1221a0')
self.wlt1 = await get_populated_wallet(entropy1)
self.wlt_svc1 = WalletService(self.wlt1)
self.fc1 = FROSTClient(self.wlt_svc1)
cfactory1 = DummyFrostJMClientProtocolFactory(self.fc1, self.nick1)
self.wlt1.set_client_factory(cfactory1)
self.wlt2 = await get_populated_wallet(entropy2)
self.wlt_svc2 = WalletService(self.wlt2)
self.fc2 = FROSTClient(self.wlt_svc2)
cfactory2 = DummyFrostJMClientProtocolFactory(self.fc2, self.nick2)
self.wlt2.set_client_factory(cfactory2)
self.wlt3 = await get_populated_wallet(entropy3)
self.wlt_svc3 = WalletService(self.wlt3)
self.fc3 = FROSTClient(self.wlt_svc3)
cfactory3 = DummyFrostJMClientProtocolFactory(self.fc3, self.nick3)
self.wlt3.set_client_factory(cfactory3)
cfactory1.add_party_client(self.nick2, cfactory2.proto_client)
cfactory1.add_party_client(self.nick3, cfactory3.proto_client)
cfactory2.add_party_client(self.nick1, cfactory1.proto_client)
cfactory2.add_party_client(self.nick3, cfactory3.proto_client)
cfactory3.add_party_client(self.nick1, cfactory1.proto_client)
cfactory3.add_party_client(self.nick2, cfactory2.proto_client)
await populate_dkg_session(self)
self.ipcs = FrostIPCServer(self.wlt1)
await self.ipcs.async_init()
self.ipcc = FrostIPCClient(self.wlt1)
await self.ipcc.async_init()
self.wlt1.set_ipc_client(self.ipcc)
class FrostIPCClientTestCase(FrostIPCTestCaseBase):
async def asyncSetUp(self):
await super().asyncSetUp()
self.serve_task = asyncio.create_task(self.ipcs.serve_forever())
async def asyncTearDown(self):
self.serve_task.cancel("cancel from asyncTearDown")
async def test_get_dkg_pubkey(self):
pubkey = await self.ipcc.get_dkg_pubkey(0, 0, 0)
dkg = self.wlt1.dkg
pubkeys = list(dkg._dkg_pubkey.values())
assert pubkey and pubkey in pubkeys
pubkey = await self.ipcc.get_dkg_pubkey(0, 0, 1)
pubkeys = list(dkg._dkg_pubkey.values())
assert pubkey and pubkey in pubkeys
async def test_frost_sign(self):
sighash = bytes.fromhex('01020304'*8)
sig, pubkey, tweaked_pubkey = await self.ipcc.frost_sign(0, 0, 0, sighash)
assert sig and len(sig) == 64
assert pubkey and len(pubkey) == 33
assert tweaked_pubkey and len(tweaked_pubkey) == 33

410
test/jmclient/test_frost_wallet.py

@ -0,0 +1,410 @@
'''Wallet functionality tests.'''
import os
import json
from pprint import pprint
from unittest import IsolatedAsyncioTestCase
import bencoder
import jmclient # install asyncioreactor
from twisted.internet import reactor
import pytest
import jmbitcoin as btc
from jmbase import get_log
from jmclient import (
load_test_config, jm_single, VolatileStorage, get_network, cryptoengine,
create_wallet, open_test_wallet_maybe, FrostWallet, DKGManager)
from jmfrost.chilldkg_ref.chilldkg import DKGOutput, hostpubkey_gen
from jmclient.frost_clients import (
serialize_ext_recovery, decrypt_ext_recovery)
pytestmark = pytest.mark.usefixtures("setup_regtest_frost_bitcoind")
test_create_wallet_filename = "frost_testwallet_for_create_wallet_test"
log = get_log()
async def get_populated_wallet():
storage = VolatileStorage()
dkg_storage = VolatileStorage()
recovery_storage = VolatileStorage()
FrostWallet.initialize(storage, dkg_storage, recovery_storage,
get_network())
wallet = FrostWallet(storage, dkg_storage, recovery_storage)
await wallet.async_init(storage)
return wallet
def populate_dkg(wlt, add_party=True, add_coordinator=True, save_dkg=True):
pubkey = hostpubkey_gen(wlt._hostseckey[:32])
md_type_idx = (0, 0, 0) # mixdepth, address_type, index
ext_recovery_bytes = serialize_ext_recovery(*md_type_idx)
ext_recovery = btc.ecies_encrypt(ext_recovery_bytes, pubkey)
if add_party:
wlt.dkg.add_party_data(
session_id=bytes.fromhex('aa'*32),
dkg_output=DKGOutput(
bytes.fromhex('01'*32), # secshare
bytes.fromhex('02'*32 + '01'), # threshold_pubkey
[ # pubshares
bytes.fromhex('03'*32 + '02'),
bytes.fromhex('03'*32 + '03'),
bytes.fromhex('03'*32 + '04'),
]
),
hostpubkeys=[
bytes.fromhex('02'*32 + '05'),
bytes.fromhex('02'*32 + '06'),
bytes.fromhex('02'*32 + '07'),
],
t=2,
recovery_data=bytes.fromhex('0102030405'*10),
ext_recovery=ext_recovery,
save_dkg=save_dkg
)
if add_coordinator:
wlt.dkg.add_coordinator_data(
session_id=bytes.fromhex('bb'*32),
dkg_output=DKGOutput(
bytes.fromhex('11'*32), # secshare
bytes.fromhex('02'*32 + '11'), # threshold_pubkey
[ # pubshares
bytes.fromhex('03'*32 + '12'),
bytes.fromhex('03'*32 + '13'),
bytes.fromhex('03'*32 + '14'),
]
),
hostpubkeys=[
bytes.fromhex('02'*32 + '15'),
bytes.fromhex('02'*32 + '16'),
bytes.fromhex('02'*32 + '17'),
],
t=2,
recovery_data=bytes.fromhex('0102030405'*10),
ext_recovery=ext_recovery,
save_dkg=save_dkg
)
return ext_recovery
def check_dkg(wlt, ext_recovery, check_party=True, check_coordinator=True):
if check_party:
dkg_dict = wlt._dkg_storage.data[DKGManager.STORAGE_KEY]
assert dkg_dict[DKGManager.SECSHARE_SUBKEY] == {
b'\xaa'*32: b'\x01'*32
}
assert dkg_dict[DKGManager.PUBSHARES_SUBKEY] == {
b'\xaa'*32: [
bytes.fromhex('03'*32 + '02'),
bytes.fromhex('03'*32 + '03'),
bytes.fromhex('03'*32 + '04'),
]
}
assert dkg_dict[DKGManager.PUBKEY_SUBKEY] == {
b'\xaa'*32: bytes.fromhex('02'*32 + '01'),
}
assert dkg_dict[DKGManager.HOSTPUBKEYS_SUBKEY] == {
b'\xaa'*32: [
bytes.fromhex('02'*32 + '05'),
bytes.fromhex('02'*32 + '06'),
bytes.fromhex('02'*32 + '07'),
]
}
assert dkg_dict[DKGManager.T_SUBKEY] == {
b'\xaa'*32: 2,
}
assert dkg_dict[DKGManager.SESSIONS_SUBKEY] == dict()
rec_dict = wlt._recovery_storage.data[DKGManager.RECOVERY_STORAGE_KEY]
assert rec_dict == {
b'\xaa'*32: [
ext_recovery,
bytes.fromhex('0102030405'*10),
],
}
if check_coordinator:
ext_recovery_bytes = decrypt_ext_recovery(wlt._hostseckey,
ext_recovery)
dkg_dict = wlt._dkg_storage.data[DKGManager.STORAGE_KEY]
assert dkg_dict[DKGManager.SECSHARE_SUBKEY] == {
b'\xbb'*32: b'\x11'*32
}
assert dkg_dict[DKGManager.PUBSHARES_SUBKEY] == {
b'\xbb'*32: [
bytes.fromhex('03'*32 + '12'),
bytes.fromhex('03'*32 + '13'),
bytes.fromhex('03'*32 + '14'),
]
}
assert dkg_dict[DKGManager.PUBKEY_SUBKEY] == {
b'\xbb'*32: bytes.fromhex('02'*32 + '11'),
}
assert dkg_dict[DKGManager.HOSTPUBKEYS_SUBKEY] == {
b'\xbb'*32: [
bytes.fromhex('02'*32 + '15'),
bytes.fromhex('02'*32 + '16'),
bytes.fromhex('02'*32 + '17'),
]
}
assert dkg_dict[DKGManager.T_SUBKEY] == {
b'\xbb'*32: 2,
}
assert dkg_dict[DKGManager.SESSIONS_SUBKEY] == {
ext_recovery_bytes: b'\xbb'*32
}
rec_dict = wlt._recovery_storage.data[DKGManager.RECOVERY_STORAGE_KEY]
assert rec_dict == {
b'\xbb'*32: [
ext_recovery,
bytes.fromhex('0102030405'*10),
],
}
class AsyncioTestCase(IsolatedAsyncioTestCase):
params = {
'test_is_standard_wallet_script': [FrostWallet]
}
def setUp(self):
load_test_config(config_path='./test_frost')
btc.select_chain_params("bitcoin/regtest")
#see note in cryptoengine.py:
cryptoengine.BTC_P2TR.VBYTE = 100
jm_single().bc_interface.tick_forward_chain_interval = 2
def tearDown(self):
if os.path.exists(test_create_wallet_filename):
os.remove(test_create_wallet_filename)
dkg_filename = f'{test_create_wallet_filename}.dkg'
recovery_filename = f'{test_create_wallet_filename}.dkg_recovery'
if os.path.exists(dkg_filename):
os.remove(dkg_filename)
if os.path.exists(recovery_filename):
os.remove(recovery_filename)
async def test_create_wallet(self):
password = b"hunter2"
wallet_name = test_create_wallet_filename
# test mainnet (we are not transacting)
btc.select_chain_params("bitcoin")
wallet = await create_wallet(wallet_name, password, 4, FrostWallet)
mnemonic = wallet.get_mnemonic_words()[0]
wallet.close()
# ensure that the wallet file created is openable with the password,
# and has the parameters that were claimed on creation:
new_wallet = await open_test_wallet_maybe(
wallet_name, "", 4, password=password, ask_for_password=False)
assert new_wallet.get_mnemonic_words()[0] == mnemonic
btc.select_chain_params("bitcoin/regtest")
async def test_dkg_manager_initialized(self):
wlt = await get_populated_wallet()
dkg_dict = wlt._dkg_storage.data[DKGManager.STORAGE_KEY]
assert set(dkg_dict.keys()) == set([
DKGManager.SECSHARE_SUBKEY,
DKGManager.PUBSHARES_SUBKEY,
DKGManager.PUBKEY_SUBKEY,
DKGManager.HOSTPUBKEYS_SUBKEY,
DKGManager.T_SUBKEY,
DKGManager.SESSIONS_SUBKEY,
])
assert dkg_dict[DKGManager.SECSHARE_SUBKEY] == dict()
assert dkg_dict[DKGManager.PUBSHARES_SUBKEY] == dict()
assert dkg_dict[DKGManager.PUBKEY_SUBKEY] == dict()
assert dkg_dict[DKGManager.HOSTPUBKEYS_SUBKEY] == dict()
assert dkg_dict[DKGManager.T_SUBKEY] == dict()
assert dkg_dict[DKGManager.SESSIONS_SUBKEY] == dict()
rec_dict = wlt._recovery_storage.data[DKGManager.RECOVERY_STORAGE_KEY]
assert rec_dict == dict()
async def test_dkg_add_party_data(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, False)
check_dkg(wlt, ext_recovery, True, False)
async def test_dkg_add_coordinator_data(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, False, True)
check_dkg(wlt, ext_recovery, False, True)
async def test_dkg_save(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True, save_dkg=False)
ext_recovery_bytes = decrypt_ext_recovery(wlt._hostseckey,
ext_recovery)
saved_dkg = bencoder.bdecode(wlt._dkg_storage.file_data[8:])
STORAGE_KEY = DKGManager.STORAGE_KEY
HOSTPUBKEYS_SUBKEY = DKGManager.HOSTPUBKEYS_SUBKEY
PUBKEY_SUBKEY = DKGManager.PUBKEY_SUBKEY
PUBSHARES_SUBKEY = DKGManager.PUBSHARES_SUBKEY
SECSHARE_SUBKEY = DKGManager.SECSHARE_SUBKEY
T_SUBKEY = DKGManager.T_SUBKEY
SESSIONS_SUBKEY = DKGManager.SESSIONS_SUBKEY
assert saved_dkg[STORAGE_KEY][SECSHARE_SUBKEY] == dict()
assert saved_dkg[STORAGE_KEY][PUBSHARES_SUBKEY] == dict()
assert saved_dkg[STORAGE_KEY][PUBKEY_SUBKEY] == dict()
assert saved_dkg[STORAGE_KEY][HOSTPUBKEYS_SUBKEY] == dict()
assert saved_dkg[STORAGE_KEY][T_SUBKEY] == dict()
assert saved_dkg[STORAGE_KEY][SESSIONS_SUBKEY] == dict()
saved_rec = bencoder.bdecode(wlt._recovery_storage.file_data[8:])
assert saved_rec[b'dkg'] == dict()
wlt.dkg.save()
saved_dkg = bencoder.bdecode(wlt._dkg_storage.file_data[8:])
assert set(saved_dkg[STORAGE_KEY][SECSHARE_SUBKEY].keys()) == set([
b'\xaa'*32,
b'\xbb'*32,
])
assert set(saved_dkg[STORAGE_KEY][PUBSHARES_SUBKEY].keys()) == set([
b'\xaa'*32,
b'\xbb'*32,
])
assert set(saved_dkg[STORAGE_KEY][PUBKEY_SUBKEY].keys()) == set([
b'\xaa'*32,
b'\xbb'*32,
])
assert set(saved_dkg[STORAGE_KEY][HOSTPUBKEYS_SUBKEY].keys()) == set([
b'\xaa'*32,
b'\xbb'*32,
])
assert set(saved_dkg[STORAGE_KEY][T_SUBKEY].keys()) == set([
b'\xaa'*32,
b'\xbb'*32,
])
assert set(saved_dkg[STORAGE_KEY][SESSIONS_SUBKEY].keys()) == set([
ext_recovery_bytes
])
saved_rec = bencoder.bdecode(wlt._recovery_storage.file_data[8:])
RECOVERY_STORAGE_KEY = DKGManager.RECOVERY_STORAGE_KEY
assert set(saved_rec[RECOVERY_STORAGE_KEY].keys()) == set([
b'\xaa'*32,
b'\xbb'*32,
])
async def test_dkg_load_storage(self):
password = b"hunter2"
wlt = await create_wallet(
test_create_wallet_filename, password, 4, FrostWallet)
mnemonic = wlt.get_mnemonic_words()[0]
ext_recovery = populate_dkg(wlt, False, True)
check_dkg(wlt, ext_recovery, False, True)
wlt.save()
wlt.close()
new_wlt = await open_test_wallet_maybe(
test_create_wallet_filename, "", 4, password=password,
ask_for_password=False,
load_dkg=True, dkg_read_only=False, read_only=True)
dkgman = DKGManager(
new_wlt, new_wlt._dkg_storage, new_wlt._recovery_storage)
new_wlt._dkg = dkgman
check_dkg(new_wlt, ext_recovery, False, True)
async def test_dkg_find_session(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True)
assert wlt.dkg.find_session(0, 0, 0) == b'\xbb'*32
assert wlt.dkg.find_session(0, 0, 1) is None
async def test_dkg_find_dkg_pubkey(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True)
assert wlt.dkg.find_dkg_pubkey(0, 0, 0) == b'\x02'*32 + b'\x11'
assert wlt.dkg.find_dkg_pubkey(0, 0, 1) is None
async def test_dkg_recover(self):
assert 0 # FIXME need to add test
async def test_dkg_ls(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True)
ls_data = wlt.dkg.dkg_ls()
ls_title = 'DKG data:\n'
ls_title_len = len(ls_title)
assert ls_data.startswith(ls_title)
ls_data = ls_data[ls_title_len:]
ls_json = json.loads(ls_data)
assert set(ls_json.keys()) == set(['sessions', 'a'*64, 'b'*64])
assert ls_json['sessions']['0,0,0'] == 'b'*64
ls_json_a = ls_json['a'*64]
ls_json_b = ls_json['b'*64]
assert ls_json_a['secshare'] == '01'*32
assert set(ls_json_a['pubshares']) == set(['03'*32 + '02',
'03'*32 + '03',
'03'*32 + '04'])
assert ls_json_a['pubkey'] == '02'*32 + '01'
assert set(ls_json_a['hostpubkeys']) == set(['02'*32 + '05',
'02'*32 + '06',
'02'*32 + '07'])
assert ls_json_a['t'] == 2
assert ls_json_b['secshare'] == '11'*32
assert set(ls_json_b['pubshares']) == set(['03'*32 + '12',
'03'*32 + '13',
'03'*32 + '14'])
assert ls_json_b['pubkey'] == '02'*32 + '11'
assert set(ls_json_b['hostpubkeys']) == set(['02'*32 + '15',
'02'*32 + '16',
'02'*32 + '17'])
assert ls_json_b['t'] == 2
async def test_dkg_rm(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True)
rm_data = wlt.dkg.dkg_rm(['a'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == f'dkg data for session {"a"*64} deleted'
rm_data = wlt.dkg.dkg_rm(['a'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == f'not found dkg data for session {"a"*64}'
rm_data = wlt.dkg.dkg_rm(['b'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == f'dkg data for session {"b"*64} deleted'
assert rm_lines[1] == f'session data for session {"b"*64} deleted'
rm_data = wlt.dkg.dkg_rm(['b'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == f'not found dkg data for session {"b"*64}'
async def test_recdkg_ls(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True)
ls_data = wlt.dkg.recdkg_ls()
ls_lines = ls_data.split('\n')
assert ls_lines[1] == 'Decrypted sesions:'
assert ls_lines[-2] == 'Not decrypted sesions:'
assert ls_lines[-1] == '[]'
ls_json = json.loads('\n'.join(ls_lines[2:-3]))
assert ls_json[0] == ['a'*64, '0'*12, '0102030405'*10]
assert ls_json[1] == ['b'*64, '0'*12, '0102030405'*10]
async def test_recdkg_rm(self):
wlt = await get_populated_wallet()
ext_recovery = populate_dkg(wlt, True, True)
rm_data = wlt.dkg.recdkg_rm(['a'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == f'dkg recovery data for session {"a"*64} deleted'
rm_data = wlt.dkg.recdkg_rm(['a'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == \
f'not found dkg recovery data for session {"a"*64}'
rm_data = wlt.dkg.recdkg_rm(['b'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == f'dkg recovery data for session {"b"*64} deleted'
rm_data = wlt.dkg.recdkg_rm(['b'*64])
rm_lines = rm_data.split('\n')
assert rm_lines[0] == \
f'not found dkg recovery data for session {"b"*64}'

1134
test/jmclient/test_taproot_wallet.py

File diff suppressed because it is too large Load Diff

21
test/jmclient/test_wallet.py

@ -530,27 +530,6 @@ class AsyncioTestCase(IsolatedAsyncioTestCase, ParametrizedTestCase):
txout = jm_single().bc_interface.pushtx(tx.serialize())
assert txout
async def test_signing_simple_p2tr(self):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
storage = VolatileStorage()
TaprootWallet.initialize(storage, get_network(), entropy=b"\xaa"*16)
wallet = TaprootWallet(storage)
await wallet.async_init(storage)
addr = await wallet.get_internal_addr(0)
utxo = fund_wallet_addr(wallet, addr)
# 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 = await wallet.get_script(
0, BaseWallet.ADDRESS_TYPE_INTERNAL, 0)
success, msg = await 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

0
test/jmfrost/chilldkg_example.py

0
test/jmfrost/test_chilldkg_ref.py

18
test/jmfrost/test_frost_ref.py

@ -2,9 +2,22 @@
import json
import os
import secrets
import sys
import time
from typing import List, Optional, Tuple
from .trusted_keygen import trusted_dealer_keygen
from jmfrost.frost_ref.reference import (
PlainPk, XonlyPk, nonce_agg, SessionContext, partial_sig_verify,
partial_sig_agg, get_xonly_pk, group_pubkey_and_tweak, individual_pk,
deterministic_sign, cbytes, sign, InvalidContributionError,
check_frost_key_compatibility, check_pubshares_correctness,
check_group_pubkey_correctness, nonce_gen_internal, AGGREGATOR_ID,
partial_sig_verify_internal, nonce_gen)
from jmfrost.frost_ref.utils.bip340 import (
schnorr_verify, bytes_from_int, int_from_bytes, point_mul, G, n)
from trusted_keygen import trusted_dealer_keygen
def fromhex_all(l):
@ -402,7 +415,8 @@ def test_sig_agg_vectors():
session_ctx = SessionContext(aggnonce_tmp, ids_tmp, pubshares_tmp, tweaks_tmp, tweak_modes_tmp, msg)
assert_raises(exception, lambda: partial_sig_agg(psigs_tmp, ids_tmp, session_ctx), except_fn)
def test_sign_and_verify_random(iterations: int) -> None:
def test_sign_and_verify_random() -> None:
iterations = 4
for itr in range(iterations):
secure_rng = secrets.SystemRandom()
# randomly choose a number: 2 <= number <= 10

152
test/jmfrost/trusted_keygen.py

@ -0,0 +1,152 @@
# Implementation of the Trusted Dealer Key Generation approach for FROST mentioned
# in https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/ (Appendix D).
#
# It's worth noting that this isn't the only compatible method (with BIP FROST Signing),
# there are alternative key generation methods available, such as BIP-FROST-DKG:
# https://github.com/BlockstreamResearch/bip-frost-dkg
# todo: use the `Scalar` type like BIP-DKG?
#todo: this shows mypy error, but the file runs
from typing import Tuple, List, NewType
import unittest
# todo: replace random module with secrets
import random
# for [1] import functions from reference
# [2] specify path for bip340 when running reference.py
# import sys, os
# script_dir = os.path.dirname(os.path.abspath(__file__))
# parent_dir = os.path.abspath(os.path.join(script_dir, '..'))
# sys.path.append(parent_dir)
from jmfrost.frost_ref.utils.bip340 import (
Point, n as curve_order, bytes_from_int,
point_mul, G, has_even_y, x
)
# point on the secret polynomial, represents a signer's secret share
PolyPoint = Tuple[int, int]
# point on the secp256k1 curve, represents a signer's public share
ECPoint = Point
#
# The following helper functions and types were copied from reference.py
#
PlainPk = NewType('PlainPk', bytes)
def xbytes(P: Point) -> bytes:
return bytes_from_int(x(P))
def cbytes(P: Point) -> bytes:
a = b'\x02' if has_even_y(P) else b'\x03'
return a + xbytes(P)
def derive_interpolating_value_internal(L: List[int], x_i: int) -> int:
num, deno = 1, 1
for x_j in L:
if x_j == x_i:
continue
num *= x_j
deno *= (x_j - x_i)
return num * pow(deno, curve_order - 2, curve_order) % curve_order
#
# End of helper functions and types copied from reference.py.
#
# evaluates poly using Horner's method, assuming coeff[0] corresponds
# to the coefficient of highest degree term
def polynomial_evaluate(coeffs: List[int], x: int) -> int:
res = 0
for coeff in coeffs:
res = res * x + coeff
return res % curve_order
def secret_share_combine(shares: List[PolyPoint]) -> int:
x_coords = []
for (x, y) in shares:
x_coords.append(x)
secret = 0
for (x, y) in shares:
delta = y * derive_interpolating_value_internal(x_coords, x)
secret += delta
return secret % curve_order
# coeffs shouldn't include the const term (i.e. secret)
def secret_share_shard(secret: int, coeffs: List[int], max_participants: int) -> List[PolyPoint]:
coeffs = coeffs + [secret]
secshares: List[PolyPoint] = []
for x_i in range(1, max_participants + 1):
y_i = polynomial_evaluate(coeffs, x_i)
secshare_i = (x_i, y_i)
secshares.append(secshare_i)
return secshares
def trusted_dealer_keygen(secret_key: int, max_participants: int, min_participants: int) -> Tuple[ECPoint, List[PolyPoint], List[ECPoint]]:
assert (1 <= secret_key <= curve_order - 1)
assert (2 <= min_participants <= max_participants)
# we don't force BIP340 compatibility of group pubkey in keygen
P = point_mul(G, secret_key)
assert P is not None
coeffs = []
for i in range(min_participants - 1):
coeffs.append(random.randint(1, curve_order - 1))
secshares = secret_share_shard(secret_key, coeffs, max_participants)
pubshares = []
for secshare in secshares:
X = point_mul(G, secshare[1])
assert X is not None
pubshares.append(X)
return (P, secshares, pubshares)
# Test vector from RFC draft.
# section F.5 of https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/15/
class Tests(unittest.TestCase):
def setUp(self) -> None:
self.max_participants = 3
self.min_participants = 2
self.poly = [
0xfbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579,
0x0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114,
]
self.shares: List[PolyPoint] = [
(1, 0x08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c),
(2, 0x04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984),
(3, 0x00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc),
]
self.secret = 0x0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114
def test_polynomial_evaluate(self) -> None:
coeffs = self.poly.copy()
expected_secret = self.secret
self.assertEqual(polynomial_evaluate(coeffs, 0), expected_secret)
def test_secret_share_combine(self) -> None:
shares: List[PolyPoint] = self.shares.copy()
expected_secret = self.secret
self.assertEqual(secret_share_combine([shares[0], shares[1]]), expected_secret)
self.assertEqual(secret_share_combine([shares[1], shares[2]]), expected_secret)
self.assertEqual(secret_share_combine([shares[0], shares[2]]), expected_secret)
self.assertEqual(secret_share_combine(shares), expected_secret)
def test_trusted_dealer_keygen(self) -> None:
secret_key = random.randint(1, curve_order - 1)
max_participants = 5
min_participants = 3
group_pk, secshares, pubshares = trusted_dealer_keygen(secret_key, max_participants, min_participants)
# group_pk need not be xonly (i.e., have even y always)
self.assertEqual(group_pk, point_mul(G, secret_key))
self.assertEqual(secret_share_combine(secshares), secret_key)
self.assertEqual(len(secshares), max_participants)
self.assertEqual(len(pubshares), max_participants)
for i in range(len(pubshares)):
with self.subTest(i=i):
self.assertEqual(pubshares[i], point_mul(G, secshares[i][1]))
if __name__=='__main__':
unittest.main()

154
test/regtest_frost_joinmarket.cfg

@ -0,0 +1,154 @@
#NOTE: This configuration file is for testing with regtest only
#For mainnet usage, running a JoinMarket script will create the default file
[DAEMON]
no_daemon = 1
daemon_port = 27183
daemon_host = localhost
use_ssl = false
[BLOCKCHAIN]
blockchain_source = regtest
rpc_host = localhost
rpc_port = 18443
rpc_user = bitcoinrpc
rpc_password = 123456abcdef
network = testnet
rpc_wallet_file = jm-test-frost-wallet
[MESSAGING:server1]
type = irc
host = localhost
hostid = localhost1
channel = joinmarket-pit
port = 16667
usessl = false
socks5 = false
socks5_host = localhost
socks5_port = 9150
[MESSAGING:server2]
type = irc
host = localhost
hostid = localhost2
channel = joinmarket-pit
port = 16668
usessl = false
socks5 = false
socks5_host = localhost
socks5_port = 9150
[MESSAGING:onion]
# onion based message channels must have the exact type 'onion'
# (while the section name above can be MESSAGING:whatever), and there must
# be only ONE such message channel configured (note the directory servers
# can be multiple, below):
type = onion
socks5_host = localhost
socks5_port = 9050
# the tor control configuration:
tor_control_host = localhost
# or, to use a UNIX socket
# control_host = unix:/var/run/tor/control
tor_control_port = 9051
# the host/port actually serving the hidden service
# (note the *virtual port*, that the client uses,
# is hardcoded to 80):
onion_serving_host = 127.0.0.1
onion_serving_port = 8080
# This is mandatory for directory nodes (who must also set their
# own .onion:port as the only directory in directory_nodes, below),
# but NOT TO BE USED by non-directory nodes (which is you, unless
# you know otherwise!), as it will greatly degrade your privacy.
#
# Special handling on regtest, so just ignore and let the code handle it:
hidden_service_dir = ""
# This is a comma separated list (comma can be omitted if only one item).
# Each item has format host:port
# On regtest we are going to increment the port numbers served from, with
# the value used here as the starting value:
directory_nodes = localhost:8081
# this is not present in default real config
# and is specifically used to flag tests:
# means we use indices 1,2,3,4,5:
regtest_count=1,5
[TIMEOUT]
maker_timeout_sec = 10
[LOGGING]
console_log_level = DEBUG
[POLICY]
segwit = true
native = true
frost = true
# for dust sweeping, try merge_algorithm = gradual
# for more rapid dust sweeping, try merge_algorithm = greedy
# for most rapid dust sweeping, try merge_algorithm = greediest
# but don't forget to bump your miner fees!
merge_algorithm = default
# the fee estimate is based on a projection of how many satoshis
# per kB are needed to get in one of the next N blocks, N set here
# as the value of 'tx_fees'. This estimate can be extremely high
# if you set N=1, so we choose N=3 for a more reasonable figure,
# as our default. Note that for clients not using a local blockchain
# instance, we retrieve an estimate from the API at cointape.com, currently.
tx_fees = 3
taker_utxo_retries = 3
taker_utxo_age = 1
taker_utxo_amtpercent = 20
accept_commitment_broadcasts = 1
#some settings useful for testing scenarios
#laxity for repeated tests; tests on actual
#commitments/maker limit/utxo sourcing logic should obviously reset
taker_utxo_retries = 3
minimum_makers = 1
listunspent_args = [0]
max_sats_freeze_reuse = -1
# ONLY for testing!
max_cj_fee_abs = 200000
max_cj_fee_rel = 0.2
[PAYJOIN]
# for the majority of situations, the defaults
# need not be altered - they will ensure you don't pay
# a significantly higher fee.
# MODIFICATION OF THESE SETTINGS IS DISADVISED.
# Payjoin protocol version; currently only '1' is supported.
payjoin_version = 1
# servers can change their destination address by default (0).
# if '1', they cannot. Note that servers can explicitly request
# that this is activated, in which case we respect that choice.
disable_output_substitution = 0
# "default" here indicates that we will allow the receiver to
# increase the fee we pay by:
# 1.2 * (our_fee_rate_per_vbyte * vsize_of_our_input_type)
# (see https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#span_idfeeoutputspanFee_output)
# (and 1.2 to give breathing room)
# which indicates we are allowing roughly one extra input's fee.
# If it is instead set to an integer, then that many satoshis are allowed.
# Additionally, note that we will also set the parameter additionafeeoutputindex
# to that of our change output, unless there is none in which case this is disabled.
max_additional_fee_contribution = default
# this is the minimum satoshis per vbyte we allow in the payjoin
# transaction; note it is decimal, not integer.
min_fee_rate = 1.1
# for payjoins to hidden service endpoints, the socks5 configuration:
onion_socks5_host = localhost
onion_socks5_port = 9050
# in some exceptional case the HS may be SSL configured,
# this feature is not yet implemented in code, but here for the
# future:
hidden_service_ssl = false
[FROST]
t = 2
hostpubkeys = 024cc1ec6fedba593a6cb683b627953b2aa80f8df80f360f78805fc00898697c74,03a8348fe4afd1974d07a50783c5d5c1ef59200eeb8ab97c7d8606534749a7043d,0307952377783138b82b222fd73199c541338a96cf758ed5a27816d6e6a324e77d

150
test/regtest_taproot_joinmarket.cfg

@ -0,0 +1,150 @@
#NOTE: This configuration file is for testing with regtest only
#For mainnet usage, running a JoinMarket script will create the default file
[DAEMON]
no_daemon = 1
daemon_port = 27183
daemon_host = localhost
use_ssl = false
[BLOCKCHAIN]
blockchain_source = regtest
rpc_host = localhost
rpc_port = 18443
rpc_user = bitcoinrpc
rpc_password = 123456abcdef
network = testnet
rpc_wallet_file = jm-test-taproot-wallet
[MESSAGING:server1]
type = irc
host = localhost
hostid = localhost1
channel = joinmarket-pit
port = 16667
usessl = false
socks5 = false
socks5_host = localhost
socks5_port = 9150
[MESSAGING:server2]
type = irc
host = localhost
hostid = localhost2
channel = joinmarket-pit
port = 16668
usessl = false
socks5 = false
socks5_host = localhost
socks5_port = 9150
[MESSAGING:onion]
# onion based message channels must have the exact type 'onion'
# (while the section name above can be MESSAGING:whatever), and there must
# be only ONE such message channel configured (note the directory servers
# can be multiple, below):
type = onion
socks5_host = localhost
socks5_port = 9050
# the tor control configuration:
tor_control_host = localhost
# or, to use a UNIX socket
# control_host = unix:/var/run/tor/control
tor_control_port = 9051
# the host/port actually serving the hidden service
# (note the *virtual port*, that the client uses,
# is hardcoded to 80):
onion_serving_host = 127.0.0.1
onion_serving_port = 8080
# This is mandatory for directory nodes (who must also set their
# own .onion:port as the only directory in directory_nodes, below),
# but NOT TO BE USED by non-directory nodes (which is you, unless
# you know otherwise!), as it will greatly degrade your privacy.
#
# Special handling on regtest, so just ignore and let the code handle it:
hidden_service_dir = ""
# This is a comma separated list (comma can be omitted if only one item).
# Each item has format host:port
# On regtest we are going to increment the port numbers served from, with
# the value used here as the starting value:
directory_nodes = localhost:8081
# this is not present in default real config
# and is specifically used to flag tests:
# means we use indices 1,2,3,4,5:
regtest_count=1,5
[TIMEOUT]
maker_timeout_sec = 10
[LOGGING]
console_log_level = DEBUG
[POLICY]
segwit = true
native = true
taproot = true
# for dust sweeping, try merge_algorithm = gradual
# for more rapid dust sweeping, try merge_algorithm = greedy
# for most rapid dust sweeping, try merge_algorithm = greediest
# but don't forget to bump your miner fees!
merge_algorithm = default
# the fee estimate is based on a projection of how many satoshis
# per kB are needed to get in one of the next N blocks, N set here
# as the value of 'tx_fees'. This estimate can be extremely high
# if you set N=1, so we choose N=3 for a more reasonable figure,
# as our default. Note that for clients not using a local blockchain
# instance, we retrieve an estimate from the API at cointape.com, currently.
tx_fees = 3
taker_utxo_retries = 3
taker_utxo_age = 1
taker_utxo_amtpercent = 20
accept_commitment_broadcasts = 1
#some settings useful for testing scenarios
#laxity for repeated tests; tests on actual
#commitments/maker limit/utxo sourcing logic should obviously reset
taker_utxo_retries = 3
minimum_makers = 1
listunspent_args = [0]
max_sats_freeze_reuse = -1
# ONLY for testing!
max_cj_fee_abs = 200000
max_cj_fee_rel = 0.2
[PAYJOIN]
# for the majority of situations, the defaults
# need not be altered - they will ensure you don't pay
# a significantly higher fee.
# MODIFICATION OF THESE SETTINGS IS DISADVISED.
# Payjoin protocol version; currently only '1' is supported.
payjoin_version = 1
# servers can change their destination address by default (0).
# if '1', they cannot. Note that servers can explicitly request
# that this is activated, in which case we respect that choice.
disable_output_substitution = 0
# "default" here indicates that we will allow the receiver to
# increase the fee we pay by:
# 1.2 * (our_fee_rate_per_vbyte * vsize_of_our_input_type)
# (see https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#span_idfeeoutputspanFee_output)
# (and 1.2 to give breathing room)
# which indicates we are allowing roughly one extra input's fee.
# If it is instead set to an integer, then that many satoshis are allowed.
# Additionally, note that we will also set the parameter additionafeeoutputindex
# to that of our change output, unless there is none in which case this is disabled.
max_additional_fee_contribution = default
# this is the minimum satoshis per vbyte we allow in the payjoin
# transaction; note it is decimal, not integer.
min_fee_rate = 1.1
# for payjoins to hidden service endpoints, the socks5 configuration:
onion_socks5_host = localhost
onion_socks5_port = 9050
# in some exceptional case the HS may be SSL configured,
# this feature is not yet implemented in code, but here for the
# future:
hidden_service_ssl = false
Loading…
Cancel
Save