From d815a6f02ccffe867c335c6c6d03c20f80002ccf Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 13 Dec 2021 00:22:18 -0500 Subject: [PATCH] use ecdsa for signing/veryfing instead of coincurve due to compatibility issues --- lbry/extras/daemon/daemon.py | 5 +-- lbry/wallet/account.py | 31 +++++++++++++------ lbry/wallet/database.py | 6 ++-- lbry/wallet/transaction.py | 12 +++---- .../blockchain/test_account_commands.py | 23 ++++---------- .../test_internal_transaction_api.py | 4 ++- tests/unit/wallet/test_schema_signing.py | 9 ++++-- 7 files changed, 48 insertions(+), 42 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 620ad36d8..ab38e8ebc 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -2859,7 +2859,9 @@ class Daemon(metaclass=JSONRPCServerType): new_txo = tx.outputs[0] if new_signing_key: - await new_txo.generate_channel_private_key() + new_txo.set_channel_private_key( + await funding_accounts[0].generate_channel_private_key() + ) else: new_txo.private_key = old_txo.private_key @@ -2868,7 +2870,6 @@ class Daemon(metaclass=JSONRPCServerType): await tx.sign(funding_accounts) if not preview: - account.add_channel_private_key(new_txo.private_key) wallet.save() await self.broadcast_or_release(tx, blocking) self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info( diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index 98abbddef..a6f2f795e 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -44,25 +44,36 @@ class DeterministicChannelKeyManager: self.cache = {} def maybe_generate_deterministic_key_for_channel(self, txo): + if self.private_key is None: + return next_key = self.private_key.child(self.last_known) - if txo.claim.channel.public_key_bytes == next_key.public_key.pubkey_bytes: - self.cache[next_key.address()] = next_key + signing_key = ecdsa.SigningKey.from_secret_exponent( + next_key.secret_exponent(), ecdsa.SECP256k1 + ) + public_key_bytes = signing_key.get_verifying_key().to_der() + if txo.claim.channel.public_key_bytes == public_key_bytes: + self.cache[self.account.ledger.public_key_to_address(public_key_bytes)] = signing_key self.last_known += 1 async def ensure_cache_primed(self): - await self.generate_next_key() + if self.private_key is not None: + await self.generate_next_key() - async def generate_next_key(self): + async def generate_next_key(self) -> ecdsa.SigningKey: db = self.account.ledger.db while True: next_key = self.private_key.child(self.last_known) - key_address = next_key.address() - self.cache[key_address] = next_key - if not await db.is_channel_key_used(self.account.wallet, key_address): - return next_key + signing_key = ecdsa.SigningKey.from_secret_exponent( + next_key.secret_exponent(), ecdsa.SECP256k1 + ) + public_key_bytes = signing_key.get_verifying_key().to_der() + key_address = self.account.ledger.public_key_to_address(public_key_bytes) + self.cache[key_address] = signing_key + if not await db.is_channel_key_used(self.account.wallet, signing_key): + return signing_key self.last_known += 1 - def get_private_key_from_pubkey_hash(self, pubkey_hash): + def get_private_key_from_pubkey_hash(self, pubkey_hash) -> ecdsa.SigningKey: return self.cache.get(pubkey_hash) @@ -561,7 +572,7 @@ class Account: channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) self.channel_keys[channel_pubkey_hash] = private_key.to_pem().decode() - async def get_channel_private_key(self, public_key_bytes): + async def get_channel_private_key(self, public_key_bytes) -> ecdsa.SigningKey: channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) private_key_pem = self.channel_keys.get(channel_pubkey_hash) if private_key_pem: diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 1f7d42f2b..9d071aba4 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -3,6 +3,7 @@ import logging import asyncio import sqlite3 import platform +import ecdsa from binascii import hexlify from collections import defaultdict from dataclasses import dataclass @@ -1241,10 +1242,11 @@ class Database(SQLiteMixin): async def set_address_history(self, address, history): await self._set_address_history(address, history) - async def is_channel_key_used(self, wallet, address): + async def is_channel_key_used(self, wallet, key: ecdsa.SigningKey): channels = await self.get_txos(wallet, txo_type=TXO_TYPES['channel']) + other_key_string = key.to_string() for channel in channels: - if channel.private_key is not None and channel.private_key.address() == address: + if channel.private_key is not None and channel.private_key.to_string() == other_key_string: return True return False diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 8c543ccf3..c9ae7fa20 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -2,7 +2,6 @@ import struct import hashlib import logging import typing -import asyncio from binascii import hexlify, unhexlify from typing import List, Iterable, Optional, Tuple @@ -28,7 +27,6 @@ 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 @@ -470,14 +468,14 @@ class Output(InputOutput): self.channel = None self.signable.clear_signature() - def set_channel_private_key(self, private_key): + def set_channel_private_key(self, private_key: ecdsa.SigningKey): self.private_key = private_key - self.claim.channel.public_key_bytes = private_key.public_key.pubkey_bytes + self.claim.channel.public_key_bytes = self.private_key.get_verifying_key().to_der() self.script.generate() - return private_key + return self.private_key - def is_channel_private_key(self, private_key): - return self.claim.channel.public_key_bytes == private_key.signing_key.to_der() + def is_channel_private_key(self, private_key: ecdsa.SigningKey): + return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der() @classmethod def pay_claim_name_pubkey_hash( diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index e9889e2be..393ee02f6 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -63,17 +63,6 @@ class AccountManagement(CommandTestCase): accounts = await self.daemon.jsonrpc_account_list(account_id, include_claims=True) self.assertEqual(accounts['items'][0]['name'], 'recreated account') - async def test_wallet_migration(self): - # null certificates should get deleted - await self.channel_create('@foo1') - await self.channel_create('@foo2') - await self.channel_create('@foo3') - keys = list(self.account.channel_keys.keys()) - self.account.channel_keys[keys[0]] = None - self.account.channel_keys[keys[1]] = "some invalid junk" - await self.account.maybe_migrate_certificates() - self.assertEqual(list(self.account.channel_keys.keys()), [keys[2]]) - async def assertFindsClaims(self, claim_names, awaitable): self.assertEqual(claim_names, [txo.claim_name for txo in (await awaitable)['items']]) @@ -207,33 +196,33 @@ class AccountManagement(CommandTestCase): self.assertTrue(channel1b.has_private_key) self.assertEqual( channel1a['outputs'][0]['value']['public_key_id'], - channel1b.private_key.public_key.address + self.ledger.public_key_to_address(channel1b.private_key.verifying_key.to_der()) ) self.assertTrue(channel2b.has_private_key) self.assertEqual( channel2a['outputs'][0]['value']['public_key_id'], - channel2b.private_key.public_key.address + self.ledger.public_key_to_address(channel2b.private_key.verifying_key.to_der()) ) # 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()) + self.assertEqual(next_key.to_string(), (await keys.generate_next_key()).to_string()) # 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()) + self.assertEqual(next_key.to_string(), (await keys.generate_next_key()).to_string()) # 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()) + self.assertNotEqual(next_key.to_string(), (await keys.generate_next_key()).to_string()) 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 + self.ledger.public_key_to_address(channel3b.private_key.verifying_key.to_der()) ) # channel key cache re-populated after simulated restart diff --git a/tests/integration/transactions/test_internal_transaction_api.py b/tests/integration/transactions/test_internal_transaction_api.py index 7f0f0c161..142009ba4 100644 --- a/tests/integration/transactions/test_internal_transaction_api.py +++ b/tests/integration/transactions/test_internal_transaction_api.py @@ -32,7 +32,9 @@ class BasicTransactionTest(IntegrationTestCase): channel_txo = Output.pay_claim_name_pubkey_hash( l2d('1.0'), '@bar', channel, self.account.ledger.address_to_hash160(address1) ) - await channel_txo.generate_channel_private_key() + channel_txo.set_channel_private_key( + await self.account.generate_channel_private_key() + ) channel_txo.script.generate() channel_tx = await Transaction.create([], [channel_txo], [self.account], self.account) diff --git a/tests/unit/wallet/test_schema_signing.py b/tests/unit/wallet/test_schema_signing.py index d0f84e865..c3ff5bdae 100644 --- a/tests/unit/wallet/test_schema_signing.py +++ b/tests/unit/wallet/test_schema_signing.py @@ -1,5 +1,7 @@ from binascii import unhexlify +import ecdsa + from lbry.testcase import AsyncioTestCase from lbry.wallet.constants import CENT, NULL_HASH32 from lbry.wallet.bip32 import PrivateKey @@ -24,10 +26,11 @@ def get_tx(): async def get_channel(claim_name='@foo'): + seed = Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '') + bip32_key = PrivateKey.from_seed(Ledger, seed) + signing_key = ecdsa.SigningKey.from_secret_exponent(bip32_key.secret_exponent(), ecdsa.SECP256k1) channel_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc') - channel_txo.set_channel_private_key(PrivateKey.from_seed( - Ledger, Mnemonic.mnemonic_to_seed(Mnemonic().make_seed(), '') - )) + channel_txo.set_channel_private_key(signing_key) get_tx().add_outputs([channel_txo]) return channel_txo