From f741b00768d032c20b0a7f769b60fa32decd7dbb Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 13 Oct 2021 10:56:10 -0400 Subject: [PATCH 01/13] progress on deterministic channel keys --- lbry/extras/daemon/daemon.py | 5 +++-- lbry/wallet/account.py | 22 +++++++++++++++++++ lbry/wallet/database.py | 6 +++++ lbry/wallet/transaction.py | 10 ++++----- .../blockchain/test_account_commands.py | 5 +++++ 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 1cd731dcb..620ad36d8 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -2704,12 +2704,13 @@ class Daemon(metaclass=JSONRPCServerType): name, claim, amount, claim_address, funding_accounts, funding_accounts[0] ) txo = tx.outputs[0] - await txo.generate_channel_private_key() + txo.set_channel_private_key( + await funding_accounts[0].generate_channel_private_key() + ) await tx.sign(funding_accounts) if not preview: - account.add_channel_private_key(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 05989c324..64d2d81af 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -34,6 +34,22 @@ def validate_claim_id(claim_id): raise Exception("Claim id is not hex encoded") +class DeterministicChannelKeyManager: + + def __init__(self, account): + self.account = account + self.public_key = account.public_key.child(2) + self.private_key = account.private_key.child(2) if account.private_key else None + + def generate_next_key(self): + db = self.account.ledger.db + i = 0 + while True: + next_key = self.private_key.child(i) + if not await db.is_channel_key_used(self.account, next_key.address): + return next_key + + class AddressManager: name: str @@ -252,6 +268,7 @@ class Account: self.receiving, self.change = self.address_generator.from_dict(self, address_generator) self.address_managers = {am.chain_number: am for am in (self.receiving, self.change)} self.channel_keys = channel_keys + self.deterministic_channel_keys = DeterministicChannelKeyManager(self) ledger.add_account(self) wallet.add_account(self) @@ -520,6 +537,11 @@ class Account: return tx + async def generate_channel_private_key(self): + key = self.deterministic_channel_keys.generate_next_key() + self.add_channel_private_key(key) + return key + def add_channel_private_key(self, private_key): public_key_bytes = private_key.get_verifying_key().to_der() channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 4507bd7dd..086e3dec8 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -1241,6 +1241,12 @@ 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, account, address): + for channel in await self.get_channels(accounts=[account]): + if channel.private_key.address == address: + return True + return False + @staticmethod def constrain_purchases(constraints): accounts = constraints.pop('accounts', None) diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index fd8ad1961..ced71f4a7 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -469,13 +469,11 @@ class Output(InputOutput): self.channel = None self.signable.clear_signature() - async def generate_channel_private_key(self): - self.private_key = await asyncio.get_event_loop().run_in_executor( - None, ecdsa.SigningKey.generate, ecdsa.SECP256k1, None, hashlib.sha256 - ) - self.claim.channel.public_key_bytes = self.private_key.get_verifying_key().to_der() + def set_channel_private_key(self, private_key): + self.private_key = private_key + self.claim.channel.public_key_bytes = private_key.get_verifying_key().to_der() self.script.generate() - return self.private_key + return private_key def is_channel_private_key(self, private_key): return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der() diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 09d57a100..5a5763cce 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -174,3 +174,8 @@ class AccountManagement(CommandTestCase): bad_address = address[0:20] + '9999999' + address[27:] 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_deterministic_channel_keys(self): + seed = self.account.seed + await self.channel_create('@foo1') + self.daemon2 = await self.add_daemon(seed=seed) From 6bd9b3744d9a6d560f8b4b0f8310b333e58d3fdd Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Fri, 15 Oct 2021 10:56:20 -0400 Subject: [PATCH 02/13] progress, channel keys generate deterministically now --- lbry/wallet/account.py | 16 +++++++++++----- lbry/wallet/database.py | 7 ++++--- lbry/wallet/transaction.py | 4 ++-- .../blockchain/test_account_commands.py | 7 ++++++- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index 64d2d81af..e577bbbcb 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -41,12 +41,19 @@ class DeterministicChannelKeyManager: self.public_key = account.public_key.child(2) self.private_key = account.private_key.child(2) if account.private_key else None - def generate_next_key(self): + async def generate_next_key(self): db = self.account.ledger.db i = 0 while True: next_key = self.private_key.child(i) - if not await db.is_channel_key_used(self.account, next_key.address): + if not await db.is_channel_key_used(self.account.wallet, next_key.address()): + return next_key + i += 1 + + def get_private_key_from_pubkey_hash(self, pubkey_hash): + for i in range(100): + next_key = self.private_key.child(i) + if next_key.address() == pubkey_hash: return next_key @@ -538,9 +545,7 @@ class Account: return tx async def generate_channel_private_key(self): - key = self.deterministic_channel_keys.generate_next_key() - self.add_channel_private_key(key) - return key + return await self.deterministic_channel_keys.generate_next_key() def add_channel_private_key(self, private_key): public_key_bytes = private_key.get_verifying_key().to_der() @@ -554,6 +559,7 @@ class Account: return await asyncio.get_event_loop().run_in_executor( None, ecdsa.SigningKey.from_pem, private_key_pem, sha256 ) + return self.deterministic_channel_keys.get_private_key_from_pubkey_hash(channel_pubkey_hash) async def maybe_migrate_certificates(self): def to_der(private_key_pem): diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 086e3dec8..603b6fc17 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -1241,9 +1241,10 @@ 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, account, address): - for channel in await self.get_channels(accounts=[account]): - if channel.private_key.address == address: + 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: return True return False diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index ced71f4a7..f4cdd6e4e 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -471,12 +471,12 @@ class Output(InputOutput): def set_channel_private_key(self, private_key): self.private_key = private_key - self.claim.channel.public_key_bytes = private_key.get_verifying_key().to_der() + self.claim.channel.public_key_bytes = private_key.public_key.pubkey_bytes self.script.generate() return private_key def is_channel_private_key(self, private_key): - return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der() + return self.claim.channel.public_key_bytes == private_key.signing_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 5a5763cce..7cf5e81ce 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -177,5 +177,10 @@ class AccountManagement(CommandTestCase): async def test_deterministic_channel_keys(self): seed = self.account.seed - await self.channel_create('@foo1') + channel1 = await self.channel_create('@foo1') + channel2 = await self.channel_create('@foo2') + self.assertNotEqual( + channel1['outputs'][0]['value']['public_key'], + channel2['outputs'][0]['value']['public_key'], + ) self.daemon2 = await self.add_daemon(seed=seed) From e4cc4521d93365cf9a15f26ec496909f691852b3 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 25 Oct 2021 10:47:48 -0400 Subject: [PATCH 03/13] channel key generation no longer arbitrarily bounded --- lbry/wallet/__init__.py | 2 +- lbry/wallet/account.py | 25 +++++++++----- lbry/wallet/ledger.py | 7 ++++ .../blockchain/test_account_commands.py | 34 ++++++++++++++++--- tests/unit/wallet/test_account.py | 6 +++- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/lbry/wallet/__init__.py b/lbry/wallet/__init__.py index c8318408f..5f2fffa21 100644 --- a/lbry/wallet/__init__.py +++ b/lbry/wallet/__init__.py @@ -10,7 +10,7 @@ from .wallet import Wallet, WalletStorage, TimestampedPreferences, ENCRYPT_ON_DI from .manager import WalletManager from .network import Network from .ledger import Ledger, RegTestLedger, TestNetLedger, BlockHeightEvent -from .account import Account, AddressManager, SingleKey, HierarchicalDeterministic +from .account import Account, AddressManager, SingleKey, HierarchicalDeterministic, DeterministicChannelKeyManager from .transaction import Transaction, Output, Input from .script import OutputScript, InputScript from .database import SQLiteMixin, Database diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index e577bbbcb..98abbddef 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -40,21 +40,30 @@ class DeterministicChannelKeyManager: self.account = account self.public_key = account.public_key.child(2) self.private_key = account.private_key.child(2) if account.private_key else None + self.last_known = 0 + self.cache = {} + + def maybe_generate_deterministic_key_for_channel(self, txo): + 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 + self.last_known += 1 + + async def ensure_cache_primed(self): + await self.generate_next_key() async def generate_next_key(self): db = self.account.ledger.db - i = 0 while True: - next_key = self.private_key.child(i) - if not await db.is_channel_key_used(self.account.wallet, next_key.address()): + 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 - i += 1 + self.last_known += 1 def get_private_key_from_pubkey_hash(self, pubkey_hash): - for i in range(100): - next_key = self.private_key.child(i) - if next_key.address() == pubkey_hash: - return next_key + return self.cache.get(pubkey_hash) class AddressManager: diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index 9583c22a7..87bace8b5 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -550,6 +550,7 @@ class Ledger(metaclass=LedgerRegistry): ) remote_history_txids = {txid for txid, _ in remote_history} async for tx in self.request_synced_transactions(to_request, remote_history_txids, address): + self.maybe_has_channel_key(tx) pending_synced_history[tx_indexes[tx.id]] = f"{tx.id}:{tx.height}:" if len(pending_synced_history) % 100 == 0: log.info("Syncing address %s: %d/%d", address, len(pending_synced_history), len(to_request)) @@ -617,6 +618,12 @@ class Ledger(metaclass=LedgerRegistry): tx.is_verified = merkle_root == header['merkle_root'] return tx + def maybe_has_channel_key(self, tx): + for txo in tx._outputs: + if txo.can_decode_claim and txo.claim.is_channel: + for account in self.accounts: + account.deterministic_channel_keys.maybe_generate_deterministic_key_for_channel(txo) + async def request_transactions(self, to_request: Tuple[Tuple[str, int], ...], cached=False): batches = [[]] remote_heights = {} diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 7cf5e81ce..5e7abec04 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -177,10 +177,36 @@ class AccountManagement(CommandTestCase): async def test_deterministic_channel_keys(self): seed = self.account.seed - channel1 = await self.channel_create('@foo1') - channel2 = await self.channel_create('@foo2') + + # create two channels and make sure they have different keys + channel1a = await self.channel_create('@foo1') + channel2a = await self.channel_create('@foo2') self.assertNotEqual( - channel1['outputs'][0]['value']['public_key'], - channel2['outputs'][0]['value']['public_key'], + channel1a['outputs'][0]['value']['public_key'], + channel2a['outputs'][0]['value']['public_key'], ) + + # start another daemon from the same seed self.daemon2 = await self.add_daemon(seed=seed) + channel2b, channel1b = (await self.daemon2.jsonrpc_channel_list())['items'] + + # both daemons end up with the same channel signing keys automagically + self.assertTrue(channel1b.has_private_key) + self.assertEqual( + channel1a['outputs'][0]['value']['public_key_id'], + channel1b.private_key.public_key.address + ) + self.assertTrue(channel2b.has_private_key) + self.assertEqual( + channel2a['outputs'][0]['value']['public_key_id'], + channel2b.private_key.public_key.address + ) + + # create third channel while both daemons running, second daemon should pick it up + channel3a = await self.channel_create('@foo3') + 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 + ) diff --git a/tests/unit/wallet/test_account.py b/tests/unit/wallet/test_account.py index 894762c68..2a4c30f7b 100644 --- a/tests/unit/wallet/test_account.py +++ b/tests/unit/wallet/test_account.py @@ -1,7 +1,11 @@ import asyncio from binascii import hexlify from lbry.testcase import AsyncioTestCase -from lbry.wallet import Wallet, Ledger, Database, Headers, Account, SingleKey, HierarchicalDeterministic +from lbry.wallet import ( + Wallet, Ledger, Database, Headers, + Account, SingleKey, HierarchicalDeterministic, + DeterministicChannelKeyManager +) class TestAccount(AsyncioTestCase): From 8216f4a87320b41d10bcc1060935489b6db554c0 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Fri, 10 Dec 2021 10:46:19 -0500 Subject: [PATCH 04/13] work in progress --- lbry/wallet/bip32.py | 4 ++ lbry/wallet/database.py | 2 +- lbry/wallet/ledger.py | 1 + lbry/wallet/transaction.py | 1 + .../blockchain/test_account_commands.py | 43 +++++++++++++++++++ tests/unit/wallet/test_schema_signing.py | 38 +++++++++++++++- 6 files changed, 87 insertions(+), 2 deletions(-) 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): From d815a6f02ccffe867c335c6c6d03c20f80002ccf Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 13 Dec 2021 00:22:18 -0500 Subject: [PATCH 05/13] 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 From d33086c8f7b50d463a6eefd1b41b27a7770dd39b Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 13 Dec 2021 00:23:53 -0500 Subject: [PATCH 06/13] deleted extraneous test --- tests/integration/blockchain/test_account_commands.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 393ee02f6..979f74ec0 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -168,14 +168,6 @@ 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 From fb57cfa5d8593c93f0cc8ed9c164705a9b5a2237 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 13 Dec 2021 00:25:21 -0500 Subject: [PATCH 07/13] moved imports for lint --- lbry/wallet/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 9d071aba4..3022deca8 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -3,13 +3,14 @@ import logging import asyncio import sqlite3 import platform -import ecdsa from binascii import hexlify from collections import defaultdict from dataclasses import dataclass from contextvars import ContextVar from typing import Tuple, List, Union, Callable, Any, Awaitable, Iterable, Dict, Optional from datetime import date + +import ecdsa from prometheus_client import Gauge, Counter, Histogram from lbry.utils import LockWithMetrics From 1eaa19536360faa3c979e3eec4fb7e36b9a1f73f Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Sun, 19 Dec 2021 16:07:01 -0500 Subject: [PATCH 08/13] reduced crypto dependency in wallet to coincurve --- lbry/extras/daemon/daemon.py | 11 ++- lbry/extras/daemon/json_response_encoder.py | 4 +- lbry/schema/claim.py | 11 ++- lbry/wallet/account.py | 90 ++++++++----------- lbry/wallet/bip32.py | 83 +++++++++++++---- lbry/wallet/database.py | 21 +++-- lbry/wallet/ledger.py | 4 +- lbry/wallet/server/block_processor.py | 2 +- lbry/wallet/transaction.py | 53 +++++------ .../blockchain/test_account_commands.py | 86 ++++++++++++++++-- .../integration/claims/test_claim_commands.py | 16 ++-- tests/unit/wallet/test_schema_signing.py | 15 ++-- 12 files changed, 251 insertions(+), 145 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index ab38e8ebc..a497a516a 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -17,7 +17,6 @@ from binascii import hexlify, unhexlify from traceback import format_exc from functools import wraps, partial -import ecdsa import base58 from aiohttp import web from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter @@ -29,6 +28,7 @@ from lbry.wallet import ( ) from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES +from lbry.wallet.bip32 import PrivateKey from lbry import utils from lbry.conf import Config, Setting, NOT_SET @@ -3041,7 +3041,7 @@ class Daemon(metaclass=JSONRPCServerType): 'channel_id': channel.claim_id, 'holding_address': address, 'holding_public_key': public_key.extended_key_string(), - 'signing_private_key': channel.private_key.to_pem().decode() + 'signing_private_key': channel.private_key.signing_key.to_pem().decode() } return base58.b58encode(json.dumps(export, separators=(',', ':'))) @@ -3064,15 +3064,14 @@ class Daemon(metaclass=JSONRPCServerType): decoded = base58.b58decode(channel_data) data = json.loads(decoded) - channel_private_key = ecdsa.SigningKey.from_pem( - data['signing_private_key'], hashfunc=hashlib.sha256 + channel_private_key = PrivateKey.from_pem( + self.ledger, data['signing_private_key'] ) - public_key_der = channel_private_key.get_verifying_key().to_der() # check that the holding_address hasn't changed since the export was made holding_address = data['holding_address'] channels, _, _, _ = await self.ledger.claim_search( - wallet.accounts, public_key_id=self.ledger.public_key_to_address(public_key_der) + wallet.accounts, public_key_id=channel_private_key.address ) if channels and channels[0].get_address(self.ledger) != holding_address: holding_address = channels[0].get_address(self.ledger) diff --git a/lbry/extras/daemon/json_response_encoder.py b/lbry/extras/daemon/json_response_encoder.py index 7e56770be..c75e7d512 100644 --- a/lbry/extras/daemon/json_response_encoder.py +++ b/lbry/extras/daemon/json_response_encoder.py @@ -10,7 +10,7 @@ from lbry.schema.claim import Claim from lbry.schema.support import Support from lbry.torrent.torrent_manager import TorrentSource from lbry.wallet import Wallet, Ledger, Account, Transaction, Output -from lbry.wallet.bip32 import PubKey +from lbry.wallet.bip32 import PublicKey from lbry.wallet.dewies import dewies_to_lbc from lbry.stream.managed_stream import ManagedStream @@ -138,7 +138,7 @@ class JSONResponseEncoder(JSONEncoder): return self.encode_claim(obj) if isinstance(obj, Support): return obj.to_dict() - if isinstance(obj, PubKey): + if isinstance(obj, PublicKey): return obj.extended_key_string() if isinstance(obj, datetime): return obj.strftime("%Y%m%dT%H:%M:%S") diff --git a/lbry/schema/claim.py b/lbry/schema/claim.py index b75945480..53565c1e1 100644 --- a/lbry/schema/claim.py +++ b/lbry/schema/claim.py @@ -2,6 +2,9 @@ import logging from typing import List from binascii import hexlify, unhexlify +from asn1crypto.keys import PublicKeyInfo +from coincurve import PublicKey as cPublicKey + from google.protobuf.json_format import MessageToDict from google.protobuf.message import DecodeError from hachoir.core.log import log as hachoir_log @@ -346,7 +349,7 @@ class Channel(BaseClaim): @property def public_key(self) -> str: - return hexlify(self.message.public_key).decode() + return hexlify(self.public_key_bytes).decode() @public_key.setter def public_key(self, sd_public_key: str): @@ -354,7 +357,11 @@ class Channel(BaseClaim): @property def public_key_bytes(self) -> bytes: - return self.message.public_key + if len(self.message.public_key) == 33: + return self.message.public_key + public_key_info = PublicKeyInfo.load(self.message.public_key) + public_key = cPublicKey(public_key_info.native['public_key']) + return public_key.format(compressed=True) @public_key_bytes.setter def public_key_bytes(self, public_key: bytes): diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index a6f2f795e..06af360d3 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -9,11 +9,10 @@ from hashlib import sha256 from string import hexdigits from typing import Type, Dict, Tuple, Optional, Any, List -import ecdsa from lbry.error import InvalidPasswordError from lbry.crypto.crypt import aes_encrypt, aes_decrypt -from .bip32 import PrivateKey, PubKey, from_extended_key_string +from .bip32 import PrivateKey, PublicKey, KeyPath, from_extended_key_string from .mnemonic import Mnemonic from .constants import COIN, TXO_TYPES from .transaction import Transaction, Input, Output @@ -36,44 +35,39 @@ def validate_claim_id(claim_id): class DeterministicChannelKeyManager: - def __init__(self, account): + def __init__(self, account: 'Account'): self.account = account - self.public_key = account.public_key.child(2) - self.private_key = account.private_key.child(2) if account.private_key else None self.last_known = 0 self.cache = {} + self.private_key: Optional[PrivateKey] = None + if account.private_key is not None: + self.private_key = account.private_key.child(KeyPath.CHANNEL) 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) - signing_key = ecdsa.SigningKey.from_secret_exponent( - next_key.secret_exponent(), ecdsa.SECP256k1 - ) - public_key_bytes = signing_key.get_verifying_key().to_der() + next_private_key = self.private_key.child(self.last_known) + public_key = next_private_key.public_key + public_key_bytes = public_key.pubkey_bytes 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.cache[public_key.address] = next_private_key self.last_known += 1 async def ensure_cache_primed(self): if self.private_key is not None: await self.generate_next_key() - async def generate_next_key(self) -> ecdsa.SigningKey: + async def generate_next_key(self) -> PrivateKey: db = self.account.ledger.db while True: - next_key = self.private_key.child(self.last_known) - 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 + next_private_key = self.private_key.child(self.last_known) + public_key = next_private_key.public_key + self.cache[public_key.address] = next_private_key + if not await db.is_channel_key_used(self.account, public_key): + return next_private_key self.last_known += 1 - def get_private_key_from_pubkey_hash(self, pubkey_hash) -> ecdsa.SigningKey: + def get_private_key_from_pubkey_hash(self, pubkey_hash) -> PrivateKey: return self.cache.get(pubkey_hash) @@ -122,7 +116,7 @@ class AddressManager: def get_private_key(self, index: int) -> PrivateKey: raise NotImplementedError - def get_public_key(self, index: int) -> PubKey: + def get_public_key(self, index: int) -> PublicKey: raise NotImplementedError async def get_max_gap(self): @@ -162,8 +156,8 @@ class HierarchicalDeterministic(AddressManager): @classmethod def from_dict(cls, account: 'Account', d: dict) -> Tuple[AddressManager, AddressManager]: return ( - cls(account, 0, **d.get('receiving', {'gap': 20, 'maximum_uses_per_address': 1})), - cls(account, 1, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1})) + cls(account, KeyPath.RECEIVE, **d.get('receiving', {'gap': 20, 'maximum_uses_per_address': 1})), + cls(account, KeyPath.CHANGE, **d.get('change', {'gap': 6, 'maximum_uses_per_address': 1})) ) def merge(self, d: dict): @@ -176,7 +170,7 @@ class HierarchicalDeterministic(AddressManager): def get_private_key(self, index: int) -> PrivateKey: return self.account.private_key.child(self.chain_number).child(index) - def get_public_key(self, index: int) -> PubKey: + def get_public_key(self, index: int) -> PublicKey: return self.account.public_key.child(self.chain_number).child(index) async def get_max_gap(self) -> int: @@ -236,7 +230,7 @@ class SingleKey(AddressManager): @classmethod def from_dict(cls, account: 'Account', d: dict) \ -> Tuple[AddressManager, AddressManager]: - same_address_manager = cls(account, account.public_key, 0) + same_address_manager = cls(account, account.public_key, KeyPath.RECEIVE) return same_address_manager, same_address_manager def to_dict_instance(self): @@ -245,7 +239,7 @@ class SingleKey(AddressManager): def get_private_key(self, index: int) -> PrivateKey: return self.account.private_key - def get_public_key(self, index: int) -> PubKey: + def get_public_key(self, index: int) -> PublicKey: return self.account.public_key async def get_max_gap(self) -> int: @@ -267,9 +261,6 @@ class SingleKey(AddressManager): class Account: - mnemonic_class = Mnemonic - private_key_class = PrivateKey - public_key_class = PubKey address_generators: Dict[str, Type[AddressManager]] = { SingleKey.name: SingleKey, HierarchicalDeterministic.name: HierarchicalDeterministic, @@ -277,7 +268,7 @@ class Account: def __init__(self, ledger: 'Ledger', wallet: 'Wallet', name: str, seed: str, private_key_string: str, encrypted: bool, - private_key: Optional[PrivateKey], public_key: PubKey, + private_key: Optional[PrivateKey], public_key: PublicKey, address_generator: dict, modified_on: float, channel_keys: dict) -> None: self.ledger = ledger self.wallet = wallet @@ -288,8 +279,8 @@ class Account: self.private_key_string = private_key_string self.init_vectors: Dict[str, bytes] = {} self.encrypted = encrypted - self.private_key = private_key - self.public_key = public_key + self.private_key: Optional[PrivateKey] = private_key + self.public_key: PublicKey = public_key generator_name = address_generator.get('name', HierarchicalDeterministic.name) self.address_generator = self.address_generators[generator_name] self.receiving, self.change = self.address_generator.from_dict(self, address_generator) @@ -310,19 +301,19 @@ class Account: name: str = None, address_generator: dict = None): return cls.from_dict(ledger, wallet, { 'name': name, - 'seed': cls.mnemonic_class().make_seed(), + 'seed': Mnemonic().make_seed(), 'address_generator': address_generator or {} }) @classmethod def get_private_key_from_seed(cls, ledger: 'Ledger', seed: str, password: str): - return cls.private_key_class.from_seed( - ledger, cls.mnemonic_class.mnemonic_to_seed(seed, password or 'lbryum') + return PrivateKey.from_seed( + ledger, Mnemonic.mnemonic_to_seed(seed, password or 'lbryum') ) @classmethod def keys_from_dict(cls, ledger: 'Ledger', d: dict) \ - -> Tuple[str, Optional[PrivateKey], PubKey]: + -> Tuple[str, Optional[PrivateKey], PublicKey]: seed = d.get('seed', '') private_key_string = d.get('private_key', '') private_key = None @@ -493,7 +484,7 @@ class Account: assert not self.encrypted, "Cannot get private key on encrypted wallet account." return self.address_managers[chain].get_private_key(index) - def get_public_key(self, chain: int, index: int) -> PubKey: + def get_public_key(self, chain: int, index: int) -> PublicKey: return self.address_managers[chain].get_public_key(index) def get_balance(self, confirmations=0, include_claims=False, read_only=False, **constraints): @@ -567,34 +558,27 @@ class Account: async def generate_channel_private_key(self): return await self.deterministic_channel_keys.generate_next_key() - def add_channel_private_key(self, private_key): - public_key_bytes = private_key.get_verifying_key().to_der() - channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) - self.channel_keys[channel_pubkey_hash] = private_key.to_pem().decode() + def add_channel_private_key(self, private_key: PrivateKey): + self.channel_keys[private_key.address] = private_key.to_pem().decode() - async def get_channel_private_key(self, public_key_bytes) -> ecdsa.SigningKey: + async def get_channel_private_key(self, public_key_bytes) -> PrivateKey: 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: - return await asyncio.get_event_loop().run_in_executor( - None, ecdsa.SigningKey.from_pem, private_key_pem, sha256 - ) + return PrivateKey.from_pem(self.ledger, private_key_pem) return self.deterministic_channel_keys.get_private_key_from_pubkey_hash(channel_pubkey_hash) async def maybe_migrate_certificates(self): - def to_der(private_key_pem): - return ecdsa.SigningKey.from_pem(private_key_pem, hashfunc=sha256).get_verifying_key().to_der() - if not self.channel_keys: return channel_keys = {} for private_key_pem in self.channel_keys.values(): if not isinstance(private_key_pem, str): continue - if "-----BEGIN EC PRIVATE KEY-----" not in private_key_pem: + if not private_key_pem.startswith("-----BEGIN"): continue - public_key_der = await asyncio.get_event_loop().run_in_executor(None, to_der, private_key_pem) - channel_keys[self.ledger.public_key_to_address(public_key_der)] = private_key_pem + private_key = PrivateKey.from_pem(self.ledger, private_key_pem) + channel_keys[private_key.address] = private_key_pem if self.channel_keys != channel_keys: self.channel_keys = channel_keys self.wallet.save() diff --git a/lbry/wallet/bip32.py b/lbry/wallet/bip32.py index 8b88aa4dd..acf0190e0 100644 --- a/lbry/wallet/bip32.py +++ b/lbry/wallet/bip32.py @@ -1,10 +1,21 @@ -from coincurve import PublicKey, PrivateKey as _PrivateKey +from asn1crypto.keys import PrivateKeyInfo, ECPrivateKey +from coincurve import PublicKey as cPublicKey, PrivateKey as cPrivateKey +from coincurve.utils import ( + pem_to_der, lib as libsecp256k1, ffi as libsecp256k1_ffi +) +from coincurve.ecdsa import CDATA_SIG_LENGTH from lbry.crypto.hash import hmac_sha512, hash160, double_sha256 from lbry.crypto.base58 import Base58 from .util import cachedproperty +class KeyPath: + RECEIVE = 0 + CHANGE = 1 + CHANNEL = 2 + + class DerivationError(Exception): """ Raised when an invalid derivation occurs. """ @@ -71,26 +82,26 @@ class _KeyBase: return Base58.encode_check(self.extended_key()) -class PubKey(_KeyBase): +class PublicKey(_KeyBase): """ A BIP32 public key. """ def __init__(self, ledger, pubkey, chain_code, n, depth, parent=None): super().__init__(ledger, chain_code, n, depth, parent) - if isinstance(pubkey, PublicKey): + if isinstance(pubkey, cPublicKey): self.verifying_key = pubkey else: self.verifying_key = self._verifying_key_from_pubkey(pubkey) @classmethod def _verifying_key_from_pubkey(cls, pubkey): - """ Converts a 33-byte compressed pubkey into an PublicKey object. """ + """ Converts a 33-byte compressed pubkey into an coincurve.PublicKey object. """ if not isinstance(pubkey, (bytes, bytearray)): raise TypeError('pubkey must be raw bytes') if len(pubkey) != 33: raise ValueError('pubkey must be 33 bytes') if pubkey[0] not in (2, 3): raise ValueError('invalid pubkey prefix byte') - return PublicKey(pubkey) + return cPublicKey(pubkey) @cachedproperty def pubkey_bytes(self): @@ -105,7 +116,7 @@ class PubKey(_KeyBase): def ec_point(self): return self.verifying_key.point() - def child(self, n: int): + def child(self, n: int) -> 'PublicKey': """ Return the derived child extended pubkey at index N. """ if not 0 <= n < (1 << 31): raise ValueError('invalid BIP32 public key child number') @@ -113,7 +124,7 @@ class PubKey(_KeyBase): msg = self.pubkey_bytes + n.to_bytes(4, 'big') L_b, R_b = self._hmac_sha512(msg) # pylint: disable=invalid-name derived_key = self.verifying_key.add(L_b) - return PubKey(self.ledger, derived_key, R_b, n, self.depth + 1, self) + return PublicKey(self.ledger, derived_key, R_b, n, self.depth + 1, self) def identifier(self): """ Return the key's identifier as 20 bytes. """ @@ -138,7 +149,7 @@ class PrivateKey(_KeyBase): def __init__(self, ledger, privkey, chain_code, n, depth, parent=None): super().__init__(ledger, chain_code, n, depth, parent) - if isinstance(privkey, _PrivateKey): + if isinstance(privkey, cPrivateKey): self.signing_key = privkey else: self.signing_key = self._signing_key_from_privkey(privkey) @@ -146,7 +157,7 @@ class PrivateKey(_KeyBase): @classmethod def _signing_key_from_privkey(cls, private_key): """ Converts a 32-byte private key into an coincurve.PrivateKey object. """ - return _PrivateKey.from_int(PrivateKey._private_key_secret_exponent(private_key)) + return cPrivateKey.from_int(PrivateKey._private_key_secret_exponent(private_key)) @classmethod def _private_key_secret_exponent(cls, private_key): @@ -158,24 +169,36 @@ class PrivateKey(_KeyBase): return int.from_bytes(private_key, 'big') @classmethod - def from_seed(cls, ledger, seed): + def from_seed(cls, ledger, seed) -> 'PrivateKey': # This hard-coded message string seems to be coin-independent... hmac = hmac_sha512(b'Bitcoin seed', seed) privkey, chain_code = hmac[:32], hmac[32:] return cls(ledger, privkey, chain_code, 0, 0) + @classmethod + def from_pem(cls, ledger, pem) -> 'PrivateKey': + der = pem_to_der(pem.encode()) + try: + key_int = ECPrivateKey.load(der).native['private_key'] + except ValueError: + key_int = PrivateKeyInfo.load(der).native['private_key']['private_key'] + private_key = cPrivateKey.from_int(key_int) + return cls(ledger, private_key, bytes((0,)*32), 0, 0) + @cachedproperty def private_key_bytes(self): """ Return the serialized private key (no leading zero byte). """ return self.signing_key.secret @cachedproperty - def public_key(self): + def public_key(self) -> PublicKey: """ Return the corresponding extended public key. """ verifying_key = self.signing_key.public_key parent_pubkey = self.parent.public_key if self.parent else None - return PubKey(self.ledger, verifying_key, self.chain_code, self.n, self.depth, - parent_pubkey) + return PublicKey( + self.ledger, verifying_key, self.chain_code, + self.n, self.depth, parent_pubkey + ) def ec_point(self): return self.public_key.ec_point() @@ -188,11 +211,12 @@ class PrivateKey(_KeyBase): """ Return the private key encoded in Wallet Import Format. """ return self.ledger.private_key_to_wif(self.private_key_bytes) + @property def address(self): """ The public key as a P2PKH address. """ return self.public_key.address - def child(self, n): + def child(self, n) -> 'PrivateKey': """ Return the derived child extended private key at index N.""" if not 0 <= n < (1 << 32): raise ValueError('invalid BIP32 private key child number') @@ -211,6 +235,28 @@ class PrivateKey(_KeyBase): """ Produce a signature for piece of data by double hashing it and signing the hash. """ return self.signing_key.sign(data, hasher=double_sha256) + def sign_compact(self, digest): + """ Produce a compact signature. """ + key = self.signing_key + + signature = libsecp256k1_ffi.new('secp256k1_ecdsa_signature *') + signed = libsecp256k1.secp256k1_ecdsa_sign( + key.context.ctx, signature, digest, key.secret, + libsecp256k1_ffi.NULL, libsecp256k1_ffi.NULL + ) + + if not signed: + raise ValueError('The private key was invalid.') + + serialized = libsecp256k1_ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH) + compacted = libsecp256k1.secp256k1_ecdsa_signature_serialize_compact( + key.context.ctx, serialized, signature + ) + if compacted != 1: + raise ValueError('The signature could not be compacted.') + + return bytes(libsecp256k1_ffi.buffer(serialized, CDATA_SIG_LENGTH)) + def identifier(self): """Return the key's identifier as 20 bytes.""" return self.public_key.identifier() @@ -222,9 +268,12 @@ class PrivateKey(_KeyBase): b'\0' + self.private_key_bytes ) + def to_pem(self): + return self.signing_key.to_pem() + def _from_extended_key(ledger, ekey): - """Return a PubKey or PrivateKey from an extended key raw bytes.""" + """Return a PublicKey or PrivateKey from an extended key raw bytes.""" if not isinstance(ekey, (bytes, bytearray)): raise TypeError('extended key must be raw bytes') if len(ekey) != 78: @@ -236,7 +285,7 @@ def _from_extended_key(ledger, ekey): if ekey[:4] == ledger.extended_public_key_prefix: pubkey = ekey[45:] - key = PubKey(ledger, pubkey, chain_code, n, depth) + key = PublicKey(ledger, pubkey, chain_code, n, depth) elif ekey[:4] == ledger.extended_private_key_prefix: if ekey[45] != 0: raise ValueError('invalid extended private key prefix byte') @@ -254,6 +303,6 @@ def from_extended_key_string(ledger, ekey_str): xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd 3qfi3t3KJtHxshXezfjft93w4UE7BGMtKwhqEHae3ZA7d823DVrL - return a PubKey or PrivateKey. + return a PublicKey or PrivateKey. """ return _from_extended_key(ledger, Base58.decode_check(ekey_str)) diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 3022deca8..d98d42f65 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -10,11 +10,10 @@ from contextvars import ContextVar from typing import Tuple, List, Union, Callable, Any, Awaitable, Iterable, Dict, Optional from datetime import date -import ecdsa from prometheus_client import Gauge, Counter, Histogram from lbry.utils import LockWithMetrics -from .bip32 import PubKey +from .bip32 import PublicKey, PrivateKey from .transaction import Transaction, Output, OutputScript, TXRefImmutable, Input from .constants import TXO_TYPES, CLAIM_TYPES from .util import date_to_julian_day @@ -977,7 +976,9 @@ class Database(SQLiteMixin): sql.append("LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid)") return await self.db.execute_fetchall(*query(' '.join(sql), **constraints), read_only=read_only) - async def get_txos(self, wallet=None, no_tx=False, no_channel_info=False, read_only=False, **constraints): + async def get_txos( + self, wallet=None, no_tx=False, no_channel_info=False, read_only=False, **constraints + ) -> List[Output]: include_is_spent = constraints.get('include_is_spent', False) include_is_my_input = constraints.get('include_is_my_input', False) include_is_my_output = constraints.pop('include_is_my_output', False) @@ -1203,7 +1204,7 @@ class Database(SQLiteMixin): addresses = await self.select_addresses(', '.join(cols), read_only=read_only, **constraints) if 'pubkey' in cols: for address in addresses: - address['pubkey'] = PubKey( + address['pubkey'] = PublicKey( self.ledger, address.pop('pubkey'), address.pop('chain_code'), address.pop('n'), address.pop('depth') ) @@ -1243,11 +1244,15 @@ 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, key: ecdsa.SigningKey): - channels = await self.get_txos(wallet, txo_type=TXO_TYPES['channel']) - other_key_string = key.to_string() + async def is_channel_key_used(self, account, key: PublicKey): + channels = await self.get_txos( + accounts=[account], txo_type=TXO_TYPES['channel'], + no_tx=True, no_channel_info=True + ) + other_key_bytes = key.pubkey_bytes for channel in channels: - if channel.private_key is not None and channel.private_key.to_string() == other_key_string: + claim = channel.can_decode_claim + if claim and claim.channel.public_key_bytes == other_key_bytes: return True return False diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index 09a649474..652c764d4 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -26,7 +26,7 @@ from .transaction import Transaction, Output from .header import Headers, UnvalidatedHeaders from .checkpoints import HASHES from .constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32 -from .bip32 import PubKey, PrivateKey +from .bip32 import PublicKey, PrivateKey from .coinselection import CoinSelector log = logging.getLogger(__name__) @@ -226,7 +226,7 @@ class Ledger(metaclass=LedgerRegistry): return account.get_private_key(address_info['chain'], address_info['pubkey'].n) return None - async def get_public_key_for_address(self, wallet, address) -> Optional[PubKey]: + async def get_public_key_for_address(self, wallet, address) -> Optional[PublicKey]: match = await self._get_account_and_address_info_for_address(wallet, address) if match: _, address_info = match diff --git a/lbry/wallet/server/block_processor.py b/lbry/wallet/server/block_processor.py index 28a2f68e9..bb233e2d5 100644 --- a/lbry/wallet/server/block_processor.py +++ b/lbry/wallet/server/block_processor.py @@ -507,7 +507,7 @@ class BlockProcessor: channel_pub_key_bytes = channel_meta.channel.public_key_bytes if channel_pub_key_bytes: channel_signature_is_valid = Output.is_signature_valid( - txo.get_encoded_signature(), txo.get_signature_digest(self.ledger), channel_pub_key_bytes + txo.signable.signature, txo.get_signature_digest(self.ledger), channel_pub_key_bytes ) if channel_signature_is_valid: self.pending_channel_counts[signing_channel_hash] += 1 diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index c9ae7fa20..7399ae674 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -1,11 +1,11 @@ import struct -import hashlib import logging import typing from binascii import hexlify, unhexlify from typing import List, Iterable, Optional, Tuple -import ecdsa +from coincurve import PublicKey as cPublicKey +from coincurve.ecdsa import deserialize_compact, cdata_to_der from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import load_der_public_key from cryptography.hazmat.primitives import hashes @@ -27,6 +27,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 PrivateKey, PublicKey if typing.TYPE_CHECKING: from lbry.wallet.account import Account @@ -221,7 +222,8 @@ class Output(InputOutput): is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None, sent_supports: Optional[int] = None, sent_tips: Optional[int] = None, received_tips: Optional[int] = None, - channel: Optional['Output'] = None, private_key: Optional[str] = None + channel: Optional['Output'] = None, + private_key: Optional[PrivateKey] = None ) -> None: super().__init__(tx_ref, position) self.amount = amount @@ -234,7 +236,7 @@ class Output(InputOutput): self.sent_tips = sent_tips self.received_tips = received_tips self.channel = channel - self.private_key = private_key + self.private_key: PrivateKey = private_key self.purchase: 'Output' = None # txo containing purchase metadata self.purchased_claim: 'Output' = None # resolved claim pointed to by purchase self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim @@ -424,25 +426,24 @@ class Output(InputOutput): ] return sha256(b''.join(pieces)) - def get_encoded_signature(self): - signature = hexlify(self.signable.signature) - r = int(signature[:int(len(signature)/2)], 16) - s = int(signature[int(len(signature)/2):], 16) - return ecdsa.util.sigencode_der(r, s, len(signature)*4) - @staticmethod - def is_signature_valid(encoded_signature, signature_digest, public_key_bytes): - try: - public_key = load_der_public_key(public_key_bytes, default_backend()) - public_key.verify(encoded_signature, signature_digest, ec.ECDSA(Prehashed(hashes.SHA256()))) - return True - except (ValueError, InvalidSignature): - pass - return False + def is_signature_valid(signature, digest, public_key_bytes): + signature = cdata_to_der(deserialize_compact(signature)) + public_key = cPublicKey(public_key_bytes) + is_valid = public_key.verify(signature, digest, None) + if not is_valid: # try old way + # ytsync signed claims don't seem to validate with coincurve + try: + pk = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_key_bytes) + pk.verify(signature, digest, ec.ECDSA(Prehashed(hashes.SHA256()))) + return True + except (ValueError, InvalidSignature): + pass + return is_valid def is_signed_by(self, channel: 'Output', ledger=None): return self.is_signature_valid( - self.get_encoded_signature(), + self.signable.signature, self.get_signature_digest(ledger), channel.claim.channel.public_key_bytes ) @@ -455,27 +456,27 @@ class Output(InputOutput): self.signable.signing_channel_hash, self.signable.to_message_bytes() ])) - self.signable.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) + self.signable.signature = channel.private_key.sign_compact(digest) self.script.generate() - def sign_data(self, data:bytes, timestamp:str) -> str: + def sign_data(self, data: bytes, timestamp: str) -> str: pieces = [timestamp.encode(), self.claim_hash, data] digest = sha256(b''.join(pieces)) - signature = self.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) + signature = self.private_key.sign_compact(digest) return hexlify(signature).decode() def clear_signature(self): self.channel = None self.signable.clear_signature() - def set_channel_private_key(self, private_key: ecdsa.SigningKey): + def set_channel_private_key(self, private_key: PrivateKey): self.private_key = private_key - self.claim.channel.public_key_bytes = self.private_key.get_verifying_key().to_der() + self.claim.channel.public_key_bytes = private_key.public_key.pubkey_bytes self.script.generate() return self.private_key - def is_channel_private_key(self, private_key: ecdsa.SigningKey): - return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der() + def is_channel_private_key(self, private_key: PrivateKey): + return self.claim.channel.public_key_bytes == private_key.public_key.pubkey_bytes @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 979f74ec0..75e65229f 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -1,7 +1,8 @@ from binascii import unhexlify from lbry.testcase import CommandTestCase -from lbry.wallet.dewies import dewies_to_lbc +from lbry.schema.claim import Claim +from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies from lbry.wallet.account import DeterministicChannelKeyManager from lbry.wallet.transaction import Transaction @@ -63,6 +64,32 @@ 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): + old_id, new_id, valid_key = ( + 'mi9E8KqFfW5ngktU22pN2jpgsdf81ZbsGY', + 'mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8', + '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95' + '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh' + '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS' + 'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n' + ) + # null certificates should get deleted + self.account.channel_keys = { + new_id: 'not valid key', + 'foo': 'bar', + } + await self.account.maybe_migrate_certificates() + self.assertEqual(self.account.channel_keys, {}) + self.account.channel_keys = { + new_id: 'not valid key', + 'foo': 'bar', + 'invalid address': valid_key, + } + await self.account.maybe_migrate_certificates() + self.assertEqual(self.account.channel_keys, { + new_id: valid_key + }) + async def assertFindsClaims(self, claim_names, awaitable): self.assertEqual(claim_names, [txo.claim_name for txo in (await awaitable)['items']]) @@ -168,6 +195,51 @@ 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 create_nondeterministic_channel(self, name, pubkey_bytes): + claim_address = await self.account.receiving.get_or_create_usable_address() + claim = Claim() + claim.channel.public_key_bytes = pubkey_bytes + tx = await Transaction.claim_create( + name, claim, lbc_to_dewies('1.0'), + claim_address, [self.account], self.account + ) + await tx.sign([self.account]) + + async def command(): + await self.daemon.broadcast_or_release(tx, False) + return tx + + return await self.confirm_and_render(command(), True) + + async def test_hybrid_channel_keys(self): + # non-deterministic channel + self.account.channel_keys = { + 'mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8': + '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95' + '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh' + '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS' + 'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n' + } + channel1 = await self.create_nondeterministic_channel('@foo1', unhexlify( + '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1' + '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16' + 'a97a6d6a4a8effd29d748901bb9789352519cd00b13d' + )) + + # deterministic channel + channel2 = await self.channel_create('@foo2') + + stream1 = await self.stream_create('stream-in-channel1', '0.01', channel_id=self.get_claim_id(channel1)) + stream2 = await self.stream_create('stream-in-channel2', '0.01', channel_id=self.get_claim_id(channel2)) + + resolved_stream1 = await self.resolve('@foo1/stream-in-channel1') + self.assertEqual('stream-in-channel1', resolved_stream1['name']) + self.assertTrue(resolved_stream1['is_channel_signature_valid']) + + resolved_stream2 = await self.resolve('@foo2/stream-in-channel2') + self.assertEqual('stream-in-channel2', resolved_stream2['name']) + self.assertTrue(resolved_stream2['is_channel_signature_valid']) + async def test_deterministic_channel_keys(self): seed = self.account.seed keys = self.account.deterministic_channel_keys @@ -188,33 +260,33 @@ class AccountManagement(CommandTestCase): self.assertTrue(channel1b.has_private_key) self.assertEqual( channel1a['outputs'][0]['value']['public_key_id'], - self.ledger.public_key_to_address(channel1b.private_key.verifying_key.to_der()) + channel1b.private_key.address ) self.assertTrue(channel2b.has_private_key) self.assertEqual( channel2a['outputs'][0]['value']['public_key_id'], - self.ledger.public_key_to_address(channel2b.private_key.verifying_key.to_der()) + channel2b.private_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.to_string(), (await keys.generate_next_key()).to_string()) + 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.to_string(), (await keys.generate_next_key()).to_string()) + 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.to_string(), (await keys.generate_next_key()).to_string()) + 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'], - self.ledger.public_key_to_address(channel3b.private_key.verifying_key.to_der()) + channel3b.private_key.address ) # channel key cache re-populated after simulated restart diff --git a/tests/integration/claims/test_claim_commands.py b/tests/integration/claims/test_claim_commands.py index 6e0a71ba5..c0abff140 100644 --- a/tests/integration/claims/test_claim_commands.py +++ b/tests/integration/claims/test_claim_commands.py @@ -19,12 +19,6 @@ from lbry.crypto.hash import sha256 log = logging.getLogger(__name__) -def get_encoded_signature(signature): - signature = signature.encode() if isinstance(signature, str) else signature - r = int(signature[:int(len(signature) / 2)], 16) - s = int(signature[int(len(signature) / 2):], 16) - return ecdsa.util.sigencode_der(r, s, len(signature) * 4) - def verify(channel, data, signature, channel_hash=None): pieces = [ @@ -33,7 +27,7 @@ def verify(channel, data, signature, channel_hash=None): data ] return Output.is_signature_valid( - get_encoded_signature(signature['signature']), + unhexlify(signature['signature']), sha256(b''.join(pieces)), channel.claim.channel.public_key_bytes ) @@ -1123,17 +1117,17 @@ class ChannelCommands(CommandTestCase): tx = await self.channel_update(claim_id, bid='4.0') self.assertEqual(tx['outputs'][0]['amount'], '4.0') - await self.assertBalance(self.account, '5.991447') + await self.assertBalance(self.account, '5.991503') # not enough funds with self.assertRaisesRegex( InsufficientFundsError, "Not enough funds to cover this transaction."): await self.channel_create('@foo2', '9.0') self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 1) - await self.assertBalance(self.account, '5.991447') + await self.assertBalance(self.account, '5.991503') # spend exactly amount available, no change - tx = await self.channel_create('@foo3', '5.981266') + tx = await self.channel_create('@foo3', '5.981322') await self.assertBalance(self.account, '0.0') self.assertEqual(len(tx['outputs']), 1) # no change self.assertItemCount(await self.daemon.jsonrpc_channel_list(), 2) @@ -1249,7 +1243,7 @@ class ChannelCommands(CommandTestCase): await daemon2.jsonrpc_channel_import(exported_data) channels = (await daemon2.jsonrpc_channel_list())['items'] self.assertEqual(1, len(channels)) - self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string()) + self.assertEqual(channel_private_key.private_key_bytes, channels[0].private_key.private_key_bytes) # second wallet can't update until channel is sent to it with self.assertRaisesRegex(AssertionError, 'Cannot find private key for signing output.'): diff --git a/tests/unit/wallet/test_schema_signing.py b/tests/unit/wallet/test_schema_signing.py index c3ff5bdae..2b2519d69 100644 --- a/tests/unit/wallet/test_schema_signing.py +++ b/tests/unit/wallet/test_schema_signing.py @@ -1,10 +1,8 @@ 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 +from lbry.wallet.bip32 import PrivateKey, KeyPath from lbry.wallet.mnemonic import Mnemonic from lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output from lbry.schema.claim import Claim @@ -27,10 +25,10 @@ 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) + key = PrivateKey.from_seed(Ledger, seed) + channel_key = key.child(KeyPath.CHANNEL).child(0) channel_txo = Output.pay_claim_name_pubkey_hash(CENT, claim_name, Claim(), b'abc') - channel_txo.set_channel_private_key(signing_key) + channel_txo.set_channel_private_key(channel_key) get_tx().add_outputs([channel_txo]) return channel_txo @@ -160,13 +158,10 @@ class TestValidateSignContent(AsyncioTestCase): some_content = "MEANINGLESS CONTENT AEE3353320".encode() timestamp_str = "1630564175" channel = await get_channel() - stream = get_stream() signature = channel.sign_data(some_content, timestamp_str) - stream.signable.signature = unhexlify(signature.encode()) - encoded_signature = stream.get_encoded_signature() pieces = [timestamp_str.encode(), channel.claim_hash, some_content] self.assertTrue(Output.is_signature_valid( - encoded_signature, + unhexlify(signature.encode()), sha256(b''.join(pieces)), channel.claim.channel.public_key_bytes )) From 23c10faff52be89dcbfc29f99dedd059385e4e46 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Sun, 19 Dec 2021 17:04:54 -0500 Subject: [PATCH 09/13] lint --- lbry/extras/daemon/daemon.py | 1 - lbry/wallet/database.py | 2 +- lbry/wallet/transaction.py | 4 +--- setup.cfg | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index a497a516a..4b1652979 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -8,7 +8,6 @@ import time import inspect import typing import random -import hashlib import tracemalloc from decimal import Decimal from urllib.parse import urlencode, quote diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index d98d42f65..2ddf9a666 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -13,7 +13,7 @@ from datetime import date from prometheus_client import Gauge, Counter, Histogram from lbry.utils import LockWithMetrics -from .bip32 import PublicKey, PrivateKey +from .bip32 import PublicKey from .transaction import Transaction, Output, OutputScript, TXRefImmutable, Input from .constants import TXO_TYPES, CLAIM_TYPES from .util import date_to_julian_day diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 7399ae674..67839aa58 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -6,8 +6,6 @@ from typing import List, Iterable, Optional, Tuple from coincurve import PublicKey as cPublicKey from coincurve.ecdsa import deserialize_compact, cdata_to_der -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import load_der_public_key from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import Prehashed @@ -27,7 +25,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 PrivateKey, PublicKey +from .bip32 import PrivateKey if typing.TYPE_CHECKING: from lbry.wallet.account import Account diff --git a/setup.cfg b/setup.cfg index f387492b2..135551a8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ ignore=words,server,rpc,schema,winpaths.py,migrator,undecorated.py max-parents=10 max-args=10 max-line-length=120 -good-names=T,t,n,i,j,k,x,y,s,f,d,h,c,e,op,db,tx,io,cachedproperty,log,id,r,iv,ts,l +good-names=T,t,n,i,j,k,x,y,s,f,d,h,c,e,op,db,tx,io,cachedproperty,log,id,r,iv,ts,l,pk valid-metaclass-classmethod-first-arg=mcs disable= c-extension-no-member, From 569f1d42b1ecd236c56725f158a302fea322a61e Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Sun, 19 Dec 2021 17:17:45 -0500 Subject: [PATCH 10/13] fix tests --- tests/unit/schema/test_claim_from_bytes.py | 4 +--- tests/unit/wallet/test_bip32.py | 24 +++++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/unit/schema/test_claim_from_bytes.py b/tests/unit/schema/test_claim_from_bytes.py index b405300d1..68664b233 100644 --- a/tests/unit/schema/test_claim_from_bytes.py +++ b/tests/unit/schema/test_claim_from_bytes.py @@ -138,9 +138,7 @@ class TestTypesV1Compatibility(TestCase): channel = cert.channel self.assertEqual( channel.public_key, - '3056301006072a8648ce3d020106052b8104000a034200043878b1edd4a1373149909ef03f4339f6da9c2b' - 'd2214c040fd2e530463ffe66098eca14fc70b50ff3aefd106049a815f595ed5a13eda7419ad78d9ed7ae47' - '3f17' + '033878b1edd4a1373149909ef03f4339f6da9c2bd2214c040fd2e530463ffe6609' ) def test_unsigned_with_fee(self): diff --git a/tests/unit/wallet/test_bip32.py b/tests/unit/wallet/test_bip32.py index 64e72c907..ef492bff4 100644 --- a/tests/unit/wallet/test_bip32.py +++ b/tests/unit/wallet/test_bip32.py @@ -1,7 +1,7 @@ from binascii import unhexlify, hexlify from lbry.testcase import AsyncioTestCase -from lbry.wallet.bip32 import PubKey, PrivateKey, from_extended_key_string +from lbry.wallet.bip32 import PublicKey, PrivateKey, from_extended_key_string from lbry.wallet import Ledger, Database, Headers from tests.unit.wallet.key_fixtures import expected_ids, expected_privkeys, expected_hardened_privkeys @@ -11,24 +11,24 @@ class BIP32Tests(AsyncioTestCase): def test_pubkey_validation(self): with self.assertRaisesRegex(TypeError, 'chain code must be raw bytes'): - PubKey(None, None, 1, None, None, None) + PublicKey(None, None, 1, None, None, None) with self.assertRaisesRegex(ValueError, 'invalid chain code'): - PubKey(None, None, b'abcd', None, None, None) + PublicKey(None, None, b'abcd', None, None, None) with self.assertRaisesRegex(ValueError, 'invalid child number'): - PubKey(None, None, b'abcd'*8, -1, None, None) + PublicKey(None, None, b'abcd'*8, -1, None, None) with self.assertRaisesRegex(ValueError, 'invalid depth'): - PubKey(None, None, b'abcd'*8, 0, 256, None) + PublicKey(None, None, b'abcd'*8, 0, 256, None) with self.assertRaisesRegex(TypeError, 'pubkey must be raw bytes'): - PubKey(None, None, b'abcd'*8, 0, 255, None) + PublicKey(None, None, b'abcd'*8, 0, 255, None) with self.assertRaisesRegex(ValueError, 'pubkey must be 33 bytes'): - PubKey(None, b'abcd', b'abcd'*8, 0, 255, None) + PublicKey(None, b'abcd', b'abcd'*8, 0, 255, None) with self.assertRaisesRegex(ValueError, 'invalid pubkey prefix byte'): - PubKey( + PublicKey( None, unhexlify('33d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'), b'abcd'*8, 0, 255, None ) - pubkey = PubKey( # success + pubkey = PublicKey( # success None, unhexlify('03d1a3dc8155673bc1e2214fa493ccc82d57961b66054af9b6b653ac28eeef3ffe'), b'abcd'*8, 0, 1, None @@ -37,7 +37,7 @@ class BIP32Tests(AsyncioTestCase): pubkey.child(-1) for i in range(20): new_key = pubkey.child(i) - self.assertIsInstance(new_key, PubKey) + self.assertIsInstance(new_key, PublicKey) self.assertEqual(hexlify(new_key.identifier()), expected_ids[i]) async def test_private_key_validation(self): @@ -60,7 +60,7 @@ class BIP32Tests(AsyncioTestCase): self.assertEqual( ec_point[1], 86198965946979720220333266272536217633917099472454294641561154971209433250106 ) - self.assertEqual('bUDcmraBp2zCV3QWmVVeQaEgepbs1b2gC9', private_key.address()) + self.assertEqual('bUDcmraBp2zCV3QWmVVeQaEgepbs1b2gC9', private_key.address) with self.assertRaisesRegex(ValueError, 'invalid BIP32 private key child number'): private_key.child(-1) self.assertIsInstance(private_key.child(PrivateKey.HARDENED), PrivateKey) @@ -100,5 +100,5 @@ class BIP32Tests(AsyncioTestCase): ledger, 'xpub661MyMwAqRbcF84AR8yfHoMzf4S2ct6mPJtvBtvNeyN9hBHuZ6uGJszkTSn5fQUCdz3XU17eBzFeAUwV6f' 'iW44g14WF52fYC5J483wqQ5ZP', - ), PubKey + ), PublicKey ) From 4822792ee2c6a3743e5c084f83fc87a8773f9849 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Sun, 19 Dec 2021 17:50:37 -0500 Subject: [PATCH 11/13] create nondetermnistic channel in test to replicate old test behavior --- lbry/testcase.py | 15 +++++++++++ .../blockchain/test_account_commands.py | 27 ++++--------------- .../blockchain/test_wallet_commands.py | 16 +++++++++-- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/lbry/testcase.py b/lbry/testcase.py index 6ad0dcb13..b10ea9b27 100644 --- a/lbry/testcase.py +++ b/lbry/testcase.py @@ -17,8 +17,10 @@ from functools import partial from lbry.wallet import WalletManager, Wallet, Ledger, Account, Transaction from lbry.conf import Config from lbry.wallet.util import satoshis_to_coins +from lbry.wallet.dewies import lbc_to_dewies from lbry.wallet.orchstr8 import Conductor from lbry.wallet.orchstr8.node import BlockchainNode, WalletNode, HubNode +from lbry.schema.claim import Claim from lbry.extras.daemon.daemon import Daemon, jsonrpc_dumps_pretty from lbry.extras.daemon.components import Component, WalletComponent @@ -506,6 +508,19 @@ class CommandTestCase(IntegrationTestCase): return self.sout(tx) return tx + async def create_nondeterministic_channel(self, name, price, pubkey_bytes, daemon=None): + account = (daemon or self.daemon).wallet_manager.default_account + claim_address = await account.receiving.get_or_create_usable_address() + claim = Claim() + claim.channel.public_key_bytes = pubkey_bytes + tx = await Transaction.claim_create( + name, claim, lbc_to_dewies(price), + claim_address, [self.account], self.account + ) + await tx.sign([self.account]) + await (daemon or self.daemon).broadcast_or_release(tx, False) + return self.sout(tx) + def create_upload_file(self, data, prefix=None, suffix=None): file_path = tempfile.mktemp(prefix=prefix or "tmp", suffix=suffix or "", dir=self.daemon.conf.upload_dir) with open(file_path, 'w+b') as file: diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 75e65229f..52eb1bfb4 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -1,10 +1,8 @@ from binascii import unhexlify from lbry.testcase import CommandTestCase -from lbry.schema.claim import Claim -from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies +from lbry.wallet.dewies import dewies_to_lbc from lbry.wallet.account import DeterministicChannelKeyManager -from lbry.wallet.transaction import Transaction def extract(d, keys): @@ -195,22 +193,6 @@ 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 create_nondeterministic_channel(self, name, pubkey_bytes): - claim_address = await self.account.receiving.get_or_create_usable_address() - claim = Claim() - claim.channel.public_key_bytes = pubkey_bytes - tx = await Transaction.claim_create( - name, claim, lbc_to_dewies('1.0'), - claim_address, [self.account], self.account - ) - await tx.sign([self.account]) - - async def command(): - await self.daemon.broadcast_or_release(tx, False) - return tx - - return await self.confirm_and_render(command(), True) - async def test_hybrid_channel_keys(self): # non-deterministic channel self.account.channel_keys = { @@ -220,17 +202,18 @@ class AccountManagement(CommandTestCase): '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS' 'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n' } - channel1 = await self.create_nondeterministic_channel('@foo1', unhexlify( + channel1 = await self.create_nondeterministic_channel('@foo1', '1.0', unhexlify( '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1' '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16' 'a97a6d6a4a8effd29d748901bb9789352519cd00b13d' )) + await self.confirm_tx(channel1['txid']) # deterministic channel channel2 = await self.channel_create('@foo2') - stream1 = await self.stream_create('stream-in-channel1', '0.01', channel_id=self.get_claim_id(channel1)) - stream2 = await self.stream_create('stream-in-channel2', '0.01', channel_id=self.get_claim_id(channel2)) + await self.stream_create('stream-in-channel1', '0.01', channel_id=self.get_claim_id(channel1)) + await self.stream_create('stream-in-channel2', '0.01', channel_id=self.get_claim_id(channel2)) resolved_stream1 = await self.resolve('@foo1/stream-in-channel1') self.assertEqual('stream-in-channel1', resolved_stream1['name']) diff --git a/tests/integration/blockchain/test_wallet_commands.py b/tests/integration/blockchain/test_wallet_commands.py index 1cce4c6b5..ab11d930d 100644 --- a/tests/integration/blockchain/test_wallet_commands.py +++ b/tests/integration/blockchain/test_wallet_commands.py @@ -1,5 +1,6 @@ import asyncio import json +from binascii import unhexlify from lbry.wallet import ENCRYPT_ON_DISK from lbry.error import InvalidPasswordError @@ -282,8 +283,19 @@ class WalletEncryptionAndSynchronization(CommandTestCase): ) # Channel Certificate - channel = await daemon2.jsonrpc_channel_create('@foo', '0.1') - await self.confirm_tx(channel.id, self.daemon2.ledger) + # non-deterministic channel + self.daemon2.wallet_manager.default_account.channel_keys['mqs77XbdnuxWN4cXrjKbSoGLkvAHa4f4B8'] = ( + '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIBZRTZ7tHnYCH3IE9mCo95' + '466L/ShYFhXGrjmSMFJw8eoAcGBSuBBAAK\noUQDQgAEmucoPz9nI+ChZrfhnh' + '0RZ/bcX0r2G0pYBmoNKovtKzXGa8y07D66MWsW\nqXptakqO/9KddIkBu5eJNS' + 'UZzQCxPQ==\n-----END EC PRIVATE KEY-----\n' + ) + channel = await self.create_nondeterministic_channel('@foo', '0.1', unhexlify( + '3056301006072a8648ce3d020106052b8104000a034200049ae7283f3f6723e0a1' + '66b7e19e1d1167f6dc5f4af61b4a58066a0d2a8bed2b35c66bccb4ec3eba316b16' + 'a97a6d6a4a8effd29d748901bb9789352519cd00b13d' + ), self.daemon2) + await self.confirm_tx(channel['txid'], self.daemon2.ledger) # both daemons will have the channel but only one has the cert so far self.assertItemCount(await daemon.jsonrpc_channel_list(), 1) From 486d5c48b020cee51e14fd6d6097a03b46d0f2c8 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Sun, 19 Dec 2021 18:03:07 -0500 Subject: [PATCH 12/13] takeover tests fix --- tests/integration/takeovers/test_resolve_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/takeovers/test_resolve_command.py b/tests/integration/takeovers/test_resolve_command.py index 42e7e3fc0..7856270ed 100644 --- a/tests/integration/takeovers/test_resolve_command.py +++ b/tests/integration/takeovers/test_resolve_command.py @@ -1721,7 +1721,7 @@ def generate_signed_legacy(address: bytes, output: Output): claim.SerializeToString(), output.claim_hash[::-1] ])) - signature = output.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) + signature = output.private_key.sign_compact(digest) claim.publisherSignature.version = 1 claim.publisherSignature.signatureType = 1 claim.publisherSignature.signature = signature From c80b30f070d7380c1a073643267607ad1d4c6b3e Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 22 Dec 2021 13:17:51 -0500 Subject: [PATCH 13/13] test another signed claim by ytsync --- tests/unit/wallet/test_schema_signing.py | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/unit/wallet/test_schema_signing.py b/tests/unit/wallet/test_schema_signing.py index 2b2519d69..901c40adb 100644 --- a/tests/unit/wallet/test_schema_signing.py +++ b/tests/unit/wallet/test_schema_signing.py @@ -120,6 +120,78 @@ class TestValidatingOldSignatures(AsyncioTestCase): self.assertTrue(stream.is_signed_by(channel, ledger)) + def test_another_signed_claim_made_by_ytsync(self): + stream_tx = Transaction(unhexlify( + b'010000000185870fabdd6bd2d57749afebc0b239e8d0ebeb6f3647d6cfcabd5ea2200ac632010000006b4' + b'83045022100877c86de154e39f21959bc2157865071924adb7930a7a8910714f27398cd2689022074270f' + b'074ae260fff319d5e0c030691821bc75b82ff0179898ac3eaeda4123eb01210200328f7f001f22ea25d72' + b'ba37379e3065020c4d8371d9199dc4e3770084e26b9ffffffff0240420f0000000000fdcc05b527746865' + b'2d637269746963616c2d6e6565642d666f722d696e646570656e64656e742d6d656469614d85050191bba' + b'd064bdc455b9ebddeeb559686b13f027615384ec7c9d981c3c21a6e3d723a654e86bd707d21174c4f697f' + b'5080cf367a3b2dfc059e6cc14a962631df69b9886f4d8b97cb339b14633966fd5ac7d75edacdf30ac5010' + b'a90010a304af34d1c1467ebfc8785e2a49c7d5bec3cc6db94db858f1dcf95e4256564fba586d6e01f496d' + b'f2a34344e021d2725ffd12197468652d637269746963616c2d6e6565642d666f722e6d703418ee97eac10' + b'22209766964656f2f6d70343230ba13e6b667a9acef7e1b1caa88b9eb1d4680dea84b1d3e838266595805' + b'ab3343855c20af35012f942ce0d5111ce080331a1f436f7079726967687465642028636f6e74616374207' + b'075626c69736865722928e2e3c98d065a0908800f10b80818f314423954686520437269746963616c204e' + b'65656420666f7220496e646570656e64656e74204d65646961207c20476c656e6e20477265656e77616c6' + b'44af006496e636c7564657320616e20696e74726f64756374696f6e20627920546f6d20576f6f64732e20' + b'5265636f7264656420696e204c616b65204a61636b736f6e2c2054657861732c206f6e20446563656d626' + b'57220342c20323032312e0a0a526f6e205061756c27732074776f2063616d706169676e7320666f722070' + b'7265736964656e7420283230303820616e64203230313229207765726520776174657273686564206d6f6' + b'd656e747320666f72206c6962657274792d6d696e6465642070656f706c652061726f756e642074686520' + b'776f726c642e205468652022526f6e205061756c205265766f6c7574696f6e22e2809463656e746572656' + b'42061726f756e642068697320756e64696c75746564206d657373616765206f662070656163652c207072' + b'6f70657274792c20616e64206d61726b657473e280946368616e6765642074686520776179206d696c6c6' + b'96f6e732074686f756768742061626f75742074686520416d65726963616e20656d7069726520616e6420' + b'74686520416d65726963616e2066696e616e6369616c2073797374656d2e2044722e205061756c2773206' + b'66f637573206f6e2063656e7472616c2062616e6b696e6720616e6420666f726569676e20706f6c696379' + b'2063617567687420706f6c6974696369616e7320616e642070756e64697473206f66662067756172642c2' + b'0666f7263696e67207468656d20746f20736372616d626c6520666f72206578706c616e6174696f6e7320' + b'6f66206f7572204d6964646c65204561737420706f6c69637920616e6420536f766965742d7374796c652' + b'063656e7472616c20706c616e6e696e6720617420746865204665642e20506f6c697469637320696e2041' + b'6d657269636120686173206e6f74206265656e207468652073616d652073696e636520746865202247697' + b'56c69616e69206d6f6d656e742220616e642022456e6420746865204665642e222054686520526f6e2050' + b'61756c205265766f6c7574696f6e2077617320626f7468206120706f6c69746963616c20616e642063756' + b'c747572616c207068656e6f6d656e6f6e2e0a0a303a303020496e74726f64756374696f6e20627920546f' + b'6d20576f6f64730a343a323720476c656e6e20477265656e77616c640a2e2e2e0a68747470733a2f2f777' + b'7772e796f75747562652e636f6d2f77617463683f763d4e4b70706d52467673453052292a276874747073' + b'3a2f2f7468756d626e61696c732e6c6272792e636f6d2f4e4b70706d5246767345305a046e6577735a096' + b'3617468656472616c5a0f636f72706f72617465206d656469615a08637269746963616c5a0f676c656e6e' + b'20677265656e77616c645a0b696e646570656e64656e745a0a6a6f75726e616c69736d5a056d656469615' + b'a056d697365735a08706f6c69746963735a0a70726f706167616e64615a08726f6e207061756c5a057472' + b'757468620208016d7576a9140969964db5b5744e2d2d0de797f5904efc80d02188acc8814200000000001' + b'976a91439086597f9cfc066f4749b8bb245bf561714fda888ac00000000' + )) + stream = stream_tx.outputs[0] + + channel_tx = Transaction(unhexlify( + b'01000000011d47b91b409b317e427adb87ec4b0bfc9fad2abf6ec3296f41918e4b3cb9d4e7010000006a4' + b'7304402205e53ef7fc643ed00f0240dd1c3302b82141f481ed071cbcdd6b6ec6166ffd4e002203eb28ce6' + b'39f80253f66ff3bf45288a60133d7f5625217d1ecf3b57da440b559f012103b852d61074eb995b702a800' + b'f284e937ece4fea7f023beb70e6b0d1bff36d64b9ffffffff0240420f0000000000fdde01b506406d6973' + b'65734db801001299010a583056301006072a8648ce3d020106052b8104000a034200047ddb1d639d7bdd0' + b'953d9ab0bf9e971a632f85f9823c1d85780aa3e0a702b503c2962d00f67360e803514bf5864710925aacb' + b'effd9597532c7e60eb21b4e3fd03223d2a3b68747470733a2f2f7468756d626e61696c732e6c6272792e6' + b'36f6d2f62616e6e65722d55436d54362d43684b7061694956753266684549734e7451420a6d697365736d' + b'656469614ad401466561747572656420766964656f732066726f6d20746865204d6973657320496e73746' + b'9747574652e20546865204d6973657320496e737469747574652070726f6d6f7465732041757374726961' + b'6e2065636f6e6f6d6963732c2066726565646f6d2c20616e6420706561636520696e20746865206c69626' + b'572616c20696e74656c6c65637475616c20747261646974696f6e206f66204c756477696720766f6e204d' + b'69736573207468726f7567682072657365617263682c207075626c697368696e672c20616e64206564756' + b'36174696f6e2e52362a3468747470733a2f2f7468756d626e61696c732e6c6272792e636f6d2f55436d54' + b'362d43684b7061694956753266684549734e74516d7576a914cd77ded2400e6569f03a2580244bb395f95' + b'f91fc88ac344ab701000000001976a914cabdbfce726d2fda92ffe0041a4303f6c6c34cda88ac00000000' + )) + channel = channel_tx.outputs[0] + + ledger = Ledger({ + 'db': Database(':memory:'), + 'headers': Headers(':memory:') + }) + + self.assertTrue(stream.is_signed_by(channel, ledger)) + def test_claim_signed_using_ecdsa_validates_with_coincurve(self): channel_tx = Transaction(unhexlify( "0100000001b91d829283c0d80cb8113d5f36b6da3dfe9df3e783f158bfb3fd1b2b178d7fc9010000006b48"