diff --git a/lbry/wallet/bip32.py b/lbry/wallet/bip32.py index 2bfcb1ad3..8b88aa4dd 100644 --- a/lbry/wallet/bip32.py +++ b/lbry/wallet/bip32.py @@ -126,6 +126,10 @@ class PubKey(_KeyBase): self.pubkey_bytes ) + def verify(self, signature, data): + """ Produce a signature for piece of data by double hashing it and signing the hash. """ + return self.verifying_key.verify(signature, data, hasher=double_sha256) + class PrivateKey(_KeyBase): """A BIP32 private key.""" diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 603b6fc17..1f7d42f2b 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -1244,7 +1244,7 @@ class Database(SQLiteMixin): async def is_channel_key_used(self, wallet, address): channels = await self.get_txos(wallet, txo_type=TXO_TYPES['channel']) for channel in channels: - if channel.private_key.address() == address: + if channel.private_key is not None and channel.private_key.address() == address: return True return False diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index 87bace8b5..09a649474 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -470,6 +470,7 @@ class Ledger(metaclass=LedgerRegistry): for address_manager in account.address_managers.values(): await self.subscribe_addresses(address_manager, await address_manager.get_addresses()) await account.ensure_address_gap() + await account.deterministic_channel_keys.ensure_cache_primed() async def unsubscribe_account(self, account: Account): for address in await account.get_addresses(): diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index f4cdd6e4e..8c543ccf3 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -28,6 +28,7 @@ from .constants import COIN, NULL_HASH32 from .bcd_data_stream import BCDataStream from .hash import TXRef, TXRefImmutable from .util import ReadOnlyList +from .bip32 import PubKey if typing.TYPE_CHECKING: from lbry.wallet.account import Account diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 5e7abec04..e9889e2be 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -1,5 +1,9 @@ +from binascii import unhexlify + from lbry.testcase import CommandTestCase from lbry.wallet.dewies import dewies_to_lbc +from lbry.wallet.account import DeterministicChannelKeyManager +from lbry.wallet.transaction import Transaction def extract(d, keys): @@ -175,8 +179,17 @@ class AccountManagement(CommandTestCase): with self.assertRaisesRegex(Exception, f"'{bad_address}' is not a valid address"): await self.daemon.jsonrpc_account_send('0.1', addresses=[bad_address]) + async def test_backwards_compatibility(self): + pk = { + 'mpAt7RQJUWe3RWPyyYQ9cinQoPH9HomPdh': + '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIMrKg13+6mj5zdqN2wCx24GgYD8PUiYVzGewgOvu24SfoA' + 'cGBSuBBAAK\noUQDQgAE1/oT/Y5X86C4eOqvPReRRNJd2+Sj5EQKZh9RtBNMahPJyYZ4/4QRky5g\n/ZfXuvA+' + 'pn68whCXIwz7IkE0iq21Xg==\n-----END EC PRIVATE KEY-----\n' + } + async def test_deterministic_channel_keys(self): seed = self.account.seed + keys = self.account.deterministic_channel_keys # create two channels and make sure they have different keys channel1a = await self.channel_create('@foo1') @@ -202,11 +215,41 @@ class AccountManagement(CommandTestCase): channel2b.private_key.public_key.address ) + # repeatedly calling next channel key returns the same key when not used + current_known = keys.last_known + next_key = await keys.generate_next_key() + self.assertEqual(current_known, keys.last_known) + self.assertEqual(next_key.address(), (await keys.generate_next_key()).address()) + # again, should be idempotent + next_key = await keys.generate_next_key() + self.assertEqual(current_known, keys.last_known) + self.assertEqual(next_key.address(), (await keys.generate_next_key()).address()) + # create third channel while both daemons running, second daemon should pick it up channel3a = await self.channel_create('@foo3') + self.assertEqual(current_known+1, keys.last_known) + self.assertNotEqual(next_key.address(), (await keys.generate_next_key()).address()) channel3b, = (await self.daemon2.jsonrpc_channel_list(name='@foo3'))['items'] self.assertTrue(channel3b.has_private_key) self.assertEqual( channel3a['outputs'][0]['value']['public_key_id'], channel3b.private_key.public_key.address ) + + # channel key cache re-populated after simulated restart + + # reset cache + self.account.deterministic_channel_keys = DeterministicChannelKeyManager(self.account) + channel3c, channel2c, channel1c = (await self.daemon.jsonrpc_channel_list())['items'] + self.assertFalse(channel1c.has_private_key) + self.assertFalse(channel2c.has_private_key) + self.assertFalse(channel3c.has_private_key) + + # repopulate cache + await self.account.deterministic_channel_keys.ensure_cache_primed() + self.assertEqual(self.account.deterministic_channel_keys.last_known, keys.last_known) + channel3c, channel2c, channel1c = (await self.daemon.jsonrpc_channel_list())['items'] + self.assertTrue(channel1c.has_private_key) + self.assertTrue(channel2c.has_private_key) + self.assertTrue(channel3c.has_private_key) + diff --git a/tests/unit/wallet/test_schema_signing.py b/tests/unit/wallet/test_schema_signing.py index 7545bb8b6..d0f84e865 100644 --- a/tests/unit/wallet/test_schema_signing.py +++ b/tests/unit/wallet/test_schema_signing.py @@ -2,10 +2,13 @@ from binascii import unhexlify from lbry.testcase import AsyncioTestCase from lbry.wallet.constants import CENT, NULL_HASH32 +from lbry.wallet.bip32 import PrivateKey +from lbry.wallet.mnemonic import Mnemonic from lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output from lbry.schema.claim import Claim from lbry.crypto.hash import sha256 + def get_output(amount=CENT, pubkey_hash=NULL_HASH32): return Transaction() \ .add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \ @@ -22,7 +25,9 @@ def get_tx(): async def get_channel(claim_name='@foo'): channel_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc') - await channel_txo.generate_channel_private_key() + channel_txo.set_channel_private_key(PrivateKey.from_seed( + Ledger, Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '') + )) get_tx().add_outputs([channel_txo]) return channel_txo @@ -114,6 +119,37 @@ class TestValidatingOldSignatures(AsyncioTestCase): self.assertTrue(stream.is_signed_by(channel, ledger)) + def test_claim_signed_using_ecdsa_validates_with_coincurve(self): + channel_tx = Transaction(unhexlify( + "0100000001b91d829283c0d80cb8113d5f36b6da3dfe9df3e783f158bfb3fd1b2b178d7fc9010000006b48" + "3045022100f4e2b4ee38388c3d3a62f4b12fdd413f6f140168e85884bbeb33a3f2d3159ef502201721200f" + "4a4f3b87484d4f47c9054e31cd3ba451dd3886a7f9f854893e7c8cf90121023f9e906e0c120f3bf74feb40" + "f01ddeafbeb1856d91938c3bef25bed06767247cffffffff0200e1f5050000000081b505406368616e4c5d" + "00125a0a583056301006072a8648ce3d020106052b8104000a03420004d7fa13fd8e57f3a0b878eaaf3d17" + "9144d25ddbe4a3e4440a661f51b4134c6a13c9c98678ff8411932e60fd97d7baf03ea67ebcc21097230cfb" + "2241348aadb55e6d7576a9149c6d700f89c77f0e8c650ba05656f8f2392782d388acf47c95350000000019" + "76a914d9502233e0e1fc76e13e36c546f704c3124d5eaa88ac00000000" + )) + channel = channel_tx.outputs[0] + + stream_tx = Transaction(unhexlify( + "010000000116a1d90763f2e3a2348c7fb438a23f232b15e3ffe3f058c3b2ab52c8bed8dcb5010000006b48" + "30450221008f38561b3a16944c63b4f4f1562f1efe1b2060f31d249e234003ee5e3461756f02205773c99e" + "83c968728e4f2433a13871c6ad23f6c10368ac52fa62a09f3f7ef5fd012102597f39845b98e2415b777aa0" + "3849d346d287af7970deb05f11214b3418ae9d82ffffffff0200e1f50500000000fd0c01b505636c61696d" + "4ce8012e6e40fa5fee1b915af3b55131dcbcebee34ab9148292b084ce3741f2e0db49783f3d854ac885f2b" + "6304a76ef7048046e338dd414ba4c64e8468651768ffaaf550c8560637ac8c477ea481ac2a9264097240f4" + "ab0a90010a8d010a3056bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454" + "f4edd1373e2b64ee2e68350d916e120b746d706c69647879363171180322186170706c69636174696f6e2f" + "6f637465742d73747265616d3230f293f5acf4310562d4a41f6620167fe6d83761a98d36738908ce5c8776" + "1642710e55352a396276a42eda92ff5856f46f6d7576a91434bd3dc4c45cc0635eb2ad5da658727e5442ca" + "0f88ace82f902f000000001976a91427b27c89eaebf68d063c107241584c07e5a6ccc688ac00000000" + )) + stream = stream_tx.outputs[0] + + ledger = Ledger({'db': Database(':memory:'), 'headers': Headers(':memory:')}) + self.assertTrue(stream.is_signed_by(channel, ledger)) + class TestValidateSignContent(AsyncioTestCase):