From f741b00768d032c20b0a7f769b60fa32decd7dbb Mon Sep 17 00:00:00 2001
From: Lex Berezhny <lex@damoti.com>
Date: Wed, 13 Oct 2021 10:56:10 -0400
Subject: [PATCH] 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)