diff --git a/jmbitcoin/jmbitcoin/__init__.py b/jmbitcoin/jmbitcoin/__init__.py index 8c5615d..999e603 100644 --- a/jmbitcoin/jmbitcoin/__init__.py +++ b/jmbitcoin/jmbitcoin/__init__.py @@ -27,10 +27,12 @@ from bitcointx.core import (x, b2x, b2lx, lx, COutPoint, CTxOut, CTxIn, coins_to_satoshi, satoshi_to_coins) from bitcointx.core.key import KeyStore from bitcointx.wallet import (P2SHCoinAddress, P2SHCoinAddressError, - P2WPKHCoinAddress, P2WPKHCoinAddressError) + P2WPKHCoinAddress, P2WPKHCoinAddressError, + CBitcoinKey) from bitcointx.core.script import (CScript, OP_0, SignatureHash, SIGHASH_ALL, SIGVERSION_WITNESS_V0, CScriptWitness) from bitcointx.core.psbt import (PartiallySignedTransaction, PSBT_Input, PSBT_Output) +from bitcointx.signmessage import SignMessage from .blocks import get_transactions_in_block diff --git a/jmclient/jmclient/cryptoengine.py b/jmclient/jmclient/cryptoengine.py index 0e3d895..3059f38 100644 --- a/jmclient/jmclient/cryptoengine.py +++ b/jmclient/jmclient/cryptoengine.py @@ -218,13 +218,19 @@ class BTCEngine(object): @staticmethod def sign_message(privkey, message): """ + Note: only (currently) used for manual + signing of text messages by keys, + *not* used in Joinmarket communication protocol. args: privkey: bytes message: bytes returns: base64-encoded signature """ - return btc.ecdsa_sign(message, privkey, True, False) + # note: only supported on mainnet + assert get_network() == "mainnet" + k = btc.CBitcoinKey(BTCEngine.privkey_to_wif(privkey)) + return btc.SignMessage(k, btc.BitcoinMessage(message)).decode("ascii") @classmethod def script_to_address(cls, script): diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index d604dcf..bbf1b19 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -1052,7 +1052,17 @@ def wallet_dumpprivkey(wallet, hdpath): return wallet.get_wif_path(path) # will raise exception on invalid path -def wallet_signmessage(wallet, hdpath, message): +def wallet_signmessage(wallet, hdpath, message, out_str=True): + """ Given a wallet, a BIP32 HD path (as can be output + from the display method) and a message string, returns + a base64 encoded signature along with the corresponding + address and message. + If `out_str` is True, returns human readable representation, + otherwise returns tuple of (signature, message, address). + """ + if not get_network() == "mainnet": + return "Error: message signing is only supported on mainnet." + msg = message.encode('utf-8') if not hdpath: @@ -1062,8 +1072,12 @@ def wallet_signmessage(wallet, hdpath, message): path = wallet.path_repr_to_path(hdpath) sig = wallet.sign_message(msg, path) - retval = "Signature: {}\nTo verify this in Bitcoin Core".format(sig) - return retval + " use the RPC command 'verifymessage'" + addr = wallet.get_address_from_path(path) + if not out_str: + return (sig, message, addr) + return ("Signature: {}\nMessage: {}\nAddress: {}\n" + "To verify this in Electrum use Tools->Sign/verify " + "message.".format(sig, message, addr)) def wallet_signpsbt(wallet_service, psbt): if not psbt: diff --git a/jmclient/test/test_walletutils.py b/jmclient/test/test_walletutils.py index d1f4184..0329c96 100644 --- a/jmclient/test/test_walletutils.py +++ b/jmclient/test/test_walletutils.py @@ -1,7 +1,39 @@ +import pytest +from jmbitcoin import select_chain_params +from jmclient import (SegwitLegacyWallet, SegwitWallet, get_network, + jm_single, VolatileStorage, load_test_config) +from jmclient.wallet_utils import (bip32pathparse, WalletView, + WalletViewAccount, WalletViewBranch, + WalletViewEntry, wallet_signmessage) -from jmclient.wallet_utils import bip32pathparse, WalletView, \ - WalletViewAccount, WalletViewBranch, WalletViewEntry - +# The below signatures have all been verified against Electrum 4.0.9: +@pytest.mark.parametrize('seed, hdpath, walletcls, message, sig, addr', [ + [b"\x01"*16, "m/84'/0'/0'/0/0", SegwitWallet, "hello", + "IOLk6ct/8aKtvTNnEAc+xojIWKv5FOwnzHGcnHkTJJwRBAyhrZ2ZyB0Re+dKS4SEav3qgjQeqMYRm+7mHi4sFKA=", + "bc1qq53d9372u8d50jfd5agq9zv7m7zdnzwducuqgz"], + [b"\x01"*16, "m/49'/0'/0'/0/0", SegwitLegacyWallet, "hello", + "HxVaQuXyBpl1UKutiusJjeLfKHwJYBzUiWuu6hEbmNFeSZGt/mbXKJ071ANR1gvdICbS/AnEa2RKDq9xMd/nU8s=", + "3AdTcqdoLHFGNq6znkahJDT41u65HAwiRv"], + [b"\x02"*16, "m/84'/0'/2'/1/0", SegwitWallet, "sign me", + "IA/V5DG7u108aNzCnpNPHqfrJAL8pF4GQ0sSqpf4Vlg5UWizauXzh2KskoD6Usl13hzqXBi4XDXl7Xxo5z6M298=", + "bc1q8mm69xs740sr0l2umrhmpl4ewhxfudxg2zvjw5"], + [b"\x02"*16, "m/49'/0'/2'/1/0", SegwitLegacyWallet, "sign me", + "H4cAtoE+zL+Mr+U8jm9DiYxZlym5xeZM3mcgymLz+TF4YYr4lgnM8qTZhFwlK4izcPaLuF27LFEoGJ/ltleIHUI=", + "3Qan1D4Vcy1yMGHfR9j7szDuC8QxSFVScA"], +]) +def test_signmessage(seed, hdpath, walletcls, message, sig, addr): + load_test_config() + jm_single().config.set('BLOCKCHAIN', 'network', 'mainnet') + select_chain_params("bitcoin/mainnet") + storage = VolatileStorage() + walletcls.initialize( + storage, get_network(), entropy=seed, max_mixdepth=3) + wallet = walletcls(storage) + s, m, a = wallet_signmessage(wallet, hdpath, message, + out_str=False) + assert (s, m, a) == (sig, message, addr) + jm_single().config.set("BLOCKCHAIN", "network", "testnet") + select_chain_params("bitcoin/regtest") def test_bip32_pathparse(): assert bip32pathparse("m/2/1/0017")