From 5777f3e15cf409eac7e5ac1c3a25c7ce735eb4d6 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Fri, 4 Mar 2022 10:53:44 -0500 Subject: [PATCH 1/7] wip --- lbry/wallet/orchstr8/node.py | 5 +++++ lbry/wallet/script.py | 20 ++++++++++++++++++- lbry/wallet/transaction.py | 16 +++++++++++++++ .../blockchain/test_account_commands.py | 17 +++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lbry/wallet/orchstr8/node.py b/lbry/wallet/orchstr8/node.py index 640a74cfb..70a157b49 100644 --- a/lbry/wallet/orchstr8/node.py +++ b/lbry/wallet/orchstr8/node.py @@ -679,6 +679,11 @@ class LBCWalletNode: def get_raw_transaction(self, txid): return self._cli_cmnd('getrawtransaction', txid, '1') + async def add_time_locked_address(self, height, address): + return json.loads( + await self._cli_cmnd('addtimelockedaddress', str(height), address) + ) + class HubProcess(asyncio.SubprocessProtocol): def __init__(self, ready, stopped): diff --git a/lbry/wallet/script.py b/lbry/wallet/script.py index 74e23aa25..dcf3cf10a 100644 --- a/lbry/wallet/script.py +++ b/lbry/wallet/script.py @@ -17,6 +17,7 @@ OP_HASH160 = 0xa9 OP_EQUALVERIFY = 0x88 OP_CHECKSIG = 0xac OP_CHECKMULTISIG = 0xae +OP_CHECKLOCKTIMEVERIFY = 0xb1 OP_EQUAL = 0x87 OP_PUSHDATA1 = 0x4c OP_PUSHDATA2 = 0x4d @@ -364,12 +365,18 @@ class InputScript(Script): REDEEM_SCRIPT_HASH = Template('script_hash', ( OP_0, PUSH_MANY('signatures'), PUSH_SUBSCRIPT('script', REDEEM_SCRIPT) )) + REDEEM_TIME_LOCK = Template('timelock', ( + SMALL_INTEGER('height'), OP_CHECKLOCKTIMEVERIFY, OP_DROP, + # rest is identical to OutputScript.PAY_PUBKEY_HASH: + OP_DUP, OP_HASH160, PUSH_SINGLE('pubkey_hash'), OP_EQUALVERIFY, OP_CHECKSIG + )) templates = [ REDEEM_PUBKEY, REDEEM_PUBKEY_HASH, REDEEM_SCRIPT_HASH, - REDEEM_SCRIPT + REDEEM_SCRIPT, + REDEEM_TIME_LOCK ] @classmethod @@ -394,6 +401,17 @@ class InputScript(Script): 'pubkeys_count': len(pubkeys) }) + @classmethod + def redeem_time_lock(cls, height, pubkey_hash): + return cls(template=cls.REDEEM_SCRIPT, values={ + 'height': height, + 'pubkey_hash': pubkey_hash + }) + + @classmethod + def redeem_time_lock_from_script(cls, script: bytes): + return cls.from_source_with_template(script, cls.REDEEM_SCRIPT) + class OutputScript(Script): diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 120482c55..59f31aec1 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -145,6 +145,12 @@ class Input(InputOutput): script = InputScript.redeem_pubkey_hash(cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY) return cls(txo.ref, script) + @classmethod + def spend_time_lock(cls, txo: 'Output', script_source: bytes) -> 'Input': + """ Create an input to spend time lock script.""" + script = InputScript.redeem_time_lock_from_script(script_source) + return cls(txo.ref, script) + @property def amount(self) -> int: """ Amount this input adds to the transaction. """ @@ -937,6 +943,16 @@ class Transaction: data = Output.add_purchase_data(Purchase(claim_id)) return cls.create([], [payment, data], funding_accounts, change_account) + @classmethod + def spend_time_lock( + cls, time_locked_txo: Output, amount: int, holding_address: str, script_source: str, + funding_accounts: List['Account'], change_account: 'Account' + ): + ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) + txi = Input.spend_time_lock(time_locked_txo, unhexlify(script_source)) + txo = Output.pay_pubkey_hash(amount, ledger.address_to_hash160(holding_address)) + return cls.create([txi], [txo], funding_accounts, change_account) + @property def my_inputs(self): for txi in self.inputs: diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 9209fe0c6..782384367 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -1,8 +1,9 @@ from binascii import unhexlify from lbry.testcase import CommandTestCase -from lbry.wallet.dewies import dewies_to_lbc +from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies from lbry.wallet.account import DeterministicChannelKeyManager +from lbry.wallet.transaction import Transaction, Input, Output def extract(d, keys): @@ -289,3 +290,17 @@ class AccountManagement(CommandTestCase): self.assertTrue(channel2c.has_private_key) self.assertTrue(channel3c.has_private_key) + async def test_time_locked_transactions(self): + from lbry.wallet.script import InputScript + address = await self.account.receiving.get_or_create_usable_address() + redeem = await self.blockchain.add_time_locked_address(210, address) + tx = await self.daemon.jsonrpc_account_send('4.0', redeem['address']) + await self.confirm_tx(tx.id) + await self.blockchain.generate(10) + txi = Input.spend(tx.outputs[0]) + txi.script.source = unhexlify(redeem['redeemScript']) + txo = Output.pay_pubkey_hash(lbc_to_dewies('3.9'), self.ledger.address_to_hash160(address)) + new_tx = await Transaction.create([txi], [txo], self.wallet.accounts, self.wallet.default_account) + src = new_tx.raw + print(new_tx.raw) + From 0cbc514a8ec17b36532d3661b4b4ac52cbc8436e Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 9 Mar 2022 10:53:55 -0500 Subject: [PATCH 2/7] account_deposit command added which accepts time locked TXs --- lbry/extras/daemon/daemon.py | 43 +++++++++++++++++++ lbry/wallet/bip32.py | 4 ++ lbry/wallet/transaction.py | 15 +++---- .../blockchain/test_account_commands.py | 23 +++++----- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index ea7a4da6f..47c62afde 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -28,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.crypto.base58 import Base58 from lbry import utils from lbry.conf import Config, Setting, NOT_SET @@ -1872,6 +1873,48 @@ class Daemon(metaclass=JSONRPCServerType): outputs=outputs, broadcast=broadcast ) + @requires("wallet") + async def jsonrpc_account_deposit( + self, txid, nout, redeem_script, private_key, + to_account=None, wallet_id=None, preview=False, blocking=False + ): + """ + Spend a time locked transaction into your account. + + Usage: + account_deposit + [ | --to_account=] + [--wallet_id=] [--preview] [--blocking] + + Options: + --txid= : (str) id of the transaction + --nout= : (int) output number in the transaction + --redeem_script= : (str) redeem script for output + --private_key= : (str) private key to sign transaction + --to_account= : (str) deposit to this account + --wallet_id= : (str) limit operation to specific wallet. + --preview : (bool) do not broadcast the transaction + --blocking : (bool) wait until tx has synced + + Returns: {Transaction} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + account = wallet.get_account_or_default(to_account) + other_tx = await self.wallet_manager.get_transaction(txid) + tx = await Transaction.spend_time_lock( + other_tx.outputs[nout], unhexlify(redeem_script), account + ) + pk = PrivateKey.from_bytes( + account.ledger, Base58.decode_check(private_key)[1:-1] + ) + tx.sign([account], {pk.address: pk}) + if not preview: + await self.broadcast_or_release(tx, blocking) + self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent()) + else: + await self.ledger.release_tx(tx) + return tx + @requires(WALLET_COMPONENT) def jsonrpc_account_send(self, amount, addresses, account_id=None, wallet_id=None, preview=False, blocking=False): """ diff --git a/lbry/wallet/bip32.py b/lbry/wallet/bip32.py index f9d96164e..1c2181b02 100644 --- a/lbry/wallet/bip32.py +++ b/lbry/wallet/bip32.py @@ -215,6 +215,10 @@ class PrivateKey(_KeyBase): private_key = cPrivateKey.from_int(key_int) return cls(ledger, private_key, bytes((0,)*32), 0, 0) + @classmethod + def from_bytes(cls, ledger, key_bytes) -> 'PrivateKey': + return cls(ledger, cPrivateKey(key_bytes), bytes((0,)*32), 0, 0) + @cachedproperty def private_key_bytes(self): """ Return the serialized private key (no leading zero byte). """ diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 59f31aec1..f504c3eac 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -860,7 +860,7 @@ class Transaction: def signature_hash_type(hash_type): return hash_type - async def sign(self, funding_accounts: Iterable['Account']): + async def sign(self, funding_accounts: Iterable['Account'], extra_keys: dict = None): self._reset() ledger, wallet = self.ensure_all_have_same_ledger_and_wallet(funding_accounts) for i, txi in enumerate(self._inputs): @@ -870,6 +870,8 @@ class Transaction: if txo_script.is_pay_pubkey_hash: address = ledger.hash160_to_address(txo_script.values['pubkey_hash']) private_key = await ledger.get_private_key_for_address(wallet, address) + if private_key is None and extra_keys: + private_key = extra_keys.get(address) assert private_key is not None, 'Cannot find private key for signing output.' tx = self._serialize_for_signature(i) txi.script.values['signature'] = \ @@ -944,14 +946,9 @@ class Transaction: return cls.create([], [payment, data], funding_accounts, change_account) @classmethod - def spend_time_lock( - cls, time_locked_txo: Output, amount: int, holding_address: str, script_source: str, - funding_accounts: List['Account'], change_account: 'Account' - ): - ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) - txi = Input.spend_time_lock(time_locked_txo, unhexlify(script_source)) - txo = Output.pay_pubkey_hash(amount, ledger.address_to_hash160(holding_address)) - return cls.create([txi], [txo], funding_accounts, change_account) + def spend_time_lock(cls, time_locked_txo: Output, script: bytes, account: 'Account'): + txi = Input.spend_time_lock(time_locked_txo, script) + return cls.create([txi], [], [account], account, sign=False) @property def my_inputs(self): diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index 782384367..ec5295118 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -1,9 +1,9 @@ from binascii import unhexlify from lbry.testcase import CommandTestCase -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, Input, Output +from lbry.crypto.base58 import Base58 def extract(d, keys): @@ -291,16 +291,17 @@ class AccountManagement(CommandTestCase): self.assertTrue(channel3c.has_private_key) async def test_time_locked_transactions(self): - from lbry.wallet.script import InputScript address = await self.account.receiving.get_or_create_usable_address() + private_key = await self.ledger.get_private_key_for_address(self.wallet, address) redeem = await self.blockchain.add_time_locked_address(210, address) + await self.assertBalance(self.account, '10.0') tx = await self.daemon.jsonrpc_account_send('4.0', redeem['address']) await self.confirm_tx(tx.id) - await self.blockchain.generate(10) - txi = Input.spend(tx.outputs[0]) - txi.script.source = unhexlify(redeem['redeemScript']) - txo = Output.pay_pubkey_hash(lbc_to_dewies('3.9'), self.ledger.address_to_hash160(address)) - new_tx = await Transaction.create([txi], [txo], self.wallet.accounts, self.wallet.default_account) - src = new_tx.raw - print(new_tx.raw) - + await self.generate(510) + await self.assertBalance(self.account, '5.999877') + tx = await self.daemon.jsonrpc_account_deposit( + tx.id, 0, redeem['redeemScript'], + Base58.encode_check(self.ledger.private_key_to_wif(private_key.private_key_bytes)) + ) + await self.confirm_tx(tx.id) + await self.assertBalance(self.account, '9.999877') From ae79314869b02f5074625ce807c7044357546ccc Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 16 Mar 2022 00:06:31 -0400 Subject: [PATCH 3/7] wip --- lbry/extras/daemon/daemon.py | 2 +- lbry/wallet/script.py | 112 ++++++++------------------ lbry/wallet/transaction.py | 15 ++-- tests/unit/wallet/test_transaction.py | 40 +++++++++ 4 files changed, 82 insertions(+), 87 deletions(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 47c62afde..6881889bc 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -1907,7 +1907,7 @@ class Daemon(metaclass=JSONRPCServerType): pk = PrivateKey.from_bytes( account.ledger, Base58.decode_check(private_key)[1:-1] ) - tx.sign([account], {pk.address: pk}) + await tx.sign([account], {pk.address: pk}) if not preview: await self.broadcast_or_release(tx, blocking) self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent()) diff --git a/lbry/wallet/script.py b/lbry/wallet/script.py index dcf3cf10a..cfcf1e5da 100644 --- a/lbry/wallet/script.py +++ b/lbry/wallet/script.py @@ -358,25 +358,27 @@ class InputScript(Script): REDEEM_PUBKEY_HASH = Template('pubkey_hash', ( PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey') )) - REDEEM_SCRIPT = Template('script', ( + MULTI_SIG_SCRIPT = Template('multi_sig', ( SMALL_INTEGER('signatures_count'), PUSH_MANY('pubkeys'), SMALL_INTEGER('pubkeys_count'), OP_CHECKMULTISIG )) - REDEEM_SCRIPT_HASH = Template('script_hash', ( - OP_0, PUSH_MANY('signatures'), PUSH_SUBSCRIPT('script', REDEEM_SCRIPT) + REDEEM_SCRIPT_HASH_MULTI_SIG = Template('script_hash+mult_sig', ( + PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey'), PUSH_SUBSCRIPT('script', MULTI_SIG_SCRIPT) )) - REDEEM_TIME_LOCK = Template('timelock', ( - SMALL_INTEGER('height'), OP_CHECKLOCKTIMEVERIFY, OP_DROP, + TIME_LOCK_SCRIPT = Template('timelock', ( + PUSH_INTEGER('height'), OP_CHECKLOCKTIMEVERIFY, OP_DROP, # rest is identical to OutputScript.PAY_PUBKEY_HASH: OP_DUP, OP_HASH160, PUSH_SINGLE('pubkey_hash'), OP_EQUALVERIFY, OP_CHECKSIG )) + REDEEM_SCRIPT_HASH_TIME_LOCK = Template('script_hash+timelock', ( + PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey'), PUSH_SUBSCRIPT('script', TIME_LOCK_SCRIPT) + )) templates = [ REDEEM_PUBKEY, REDEEM_PUBKEY_HASH, - REDEEM_SCRIPT_HASH, - REDEEM_SCRIPT, - REDEEM_TIME_LOCK + REDEEM_SCRIPT_HASH_TIME_LOCK, + REDEEM_SCRIPT_HASH_MULTI_SIG, ] @classmethod @@ -387,31 +389,33 @@ class InputScript(Script): }) @classmethod - def redeem_script_hash(cls, signatures, pubkeys): - return cls(template=cls.REDEEM_SCRIPT_HASH, values={ + def redeem_mult_sig_script_hash(cls, signatures, pubkeys): + return cls(template=cls.REDEEM_SCRIPT_HASH_MULTI_SIG, values={ 'signatures': signatures, - 'script': cls.redeem_script(signatures, pubkeys) + 'script': cls(template=cls.MULTI_SIG_SCRIPT, values={ + 'signatures_count': len(signatures), + 'pubkeys': pubkeys, + 'pubkeys_count': len(pubkeys) + }) }) @classmethod - def redeem_script(cls, signatures, pubkeys): - return cls(template=cls.REDEEM_SCRIPT, values={ - 'signatures_count': len(signatures), - 'pubkeys': pubkeys, - 'pubkeys_count': len(pubkeys) + def redeem_time_lock_script_hash(cls, signature, pubkey, height=None, pubkey_hash=None, script_source=None): + if height and pubkey_hash: + script = cls(template=cls.TIME_LOCK_SCRIPT, values={ + 'height': height, + 'pubkey_hash': pubkey_hash + }) + elif script_source: + script = cls(source=script_source, template=cls.TIME_LOCK_SCRIPT) + else: + raise ValueError("script_source or both height and pubkey_hash are required.") + return cls(template=cls.REDEEM_SCRIPT_HASH_TIME_LOCK, values={ + 'signature': signature, + 'pubkey': pubkey, + 'script': script }) - @classmethod - def redeem_time_lock(cls, height, pubkey_hash): - return cls(template=cls.REDEEM_SCRIPT, values={ - 'height': height, - 'pubkey_hash': pubkey_hash - }) - - @classmethod - def redeem_time_lock_from_script(cls, script: bytes): - return cls.from_source_with_template(script, cls.REDEEM_SCRIPT) - class OutputScript(Script): @@ -478,21 +482,6 @@ class OutputScript(Script): UPDATE_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes )) - SELL_SCRIPT = Template('sell_script', ( - OP_VERIFY, OP_DROP, OP_DROP, OP_DROP, PUSH_INTEGER('price'), OP_PRICECHECK - )) - SELL_CLAIM = Template('sell_claim+pay_script_hash', ( - OP_SELL_CLAIM, PUSH_SINGLE('claim_id'), PUSH_SUBSCRIPT('sell_script', SELL_SCRIPT), - PUSH_SUBSCRIPT('receive_script', InputScript.REDEEM_SCRIPT), OP_2DROP, OP_2DROP - ) + PAY_SCRIPT_HASH.opcodes) - - BUY_CLAIM = Template('buy_claim+pay_script_hash', ( - OP_BUY_CLAIM, PUSH_SINGLE('sell_id'), - PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim_version'), - PUSH_SINGLE('owner_pubkey_hash'), PUSH_SINGLE('negotiation_signature'), - OP_2DROP, OP_2DROP, OP_2DROP, - ) + PAY_SCRIPT_HASH.opcodes) - templates = [ PAY_PUBKEY_FULL, PAY_PUBKEY_HASH, @@ -507,8 +496,6 @@ class OutputScript(Script): SUPPORT_CLAIM_DATA_SCRIPT, UPDATE_CLAIM_PUBKEY, UPDATE_CLAIM_SCRIPT, - SELL_CLAIM, SELL_SCRIPT, - BUY_CLAIM, ] @classmethod @@ -568,30 +555,6 @@ class OutputScript(Script): 'pubkey_hash': pubkey_hash }) - @classmethod - def sell_script(cls, price): - return cls(template=cls.SELL_SCRIPT, values={ - 'price': price, - }) - - @classmethod - def sell_claim(cls, claim_id, price, signatures, pubkeys): - return cls(template=cls.SELL_CLAIM, values={ - 'claim_id': claim_id, - 'sell_script': OutputScript.sell_script(price), - 'receive_script': InputScript.redeem_script(signatures, pubkeys) - }) - - @classmethod - def buy_claim(cls, sell_id, claim_id, claim_version, owner_pubkey_hash, negotiation_signature): - return cls(template=cls.BUY_CLAIM, values={ - 'sell_id': sell_id, - 'claim_id': claim_id, - 'claim_version': claim_version, - 'owner_pubkey_hash': owner_pubkey_hash, - 'negotiation_signature': negotiation_signature, - }) - @property def is_pay_pubkey_hash(self): return self.template.name.endswith('pay_pubkey_hash') @@ -620,17 +583,6 @@ class OutputScript(Script): def is_support_claim_data(self): return self.template.name.startswith('support_claim+data+') - @property - def is_sell_claim(self): - return self.template.name.startswith('sell_claim+') - - @property - def is_buy_claim(self): - return self.template.name.startswith('buy_claim+') - @property def is_claim_involved(self): - return any(( - self.is_claim_name, self.is_support_claim, self.is_update_claim, - self.is_sell_claim, self.is_buy_claim - )) + return any((self.is_claim_name, self.is_support_claim, self.is_update_claim)) diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index f504c3eac..fdc225a06 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -148,7 +148,9 @@ class Input(InputOutput): @classmethod def spend_time_lock(cls, txo: 'Output', script_source: bytes) -> 'Input': """ Create an input to spend time lock script.""" - script = InputScript.redeem_time_lock_from_script(script_source) + script = InputScript.redeem_time_lock_script_hash( + cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY, script_source=script_source + ) return cls(txo.ref, script) @property @@ -867,11 +869,12 @@ class Transaction: assert txi.script is not None assert txi.txo_ref.txo is not None txo_script = txi.txo_ref.txo.script - if txo_script.is_pay_pubkey_hash: - address = ledger.hash160_to_address(txo_script.values['pubkey_hash']) - private_key = await ledger.get_private_key_for_address(wallet, address) - if private_key is None and extra_keys: - private_key = extra_keys.get(address) + if txo_script.is_pay_pubkey_hash or txo_script.is_pay_script_hash: + if 'pubkey_hash' in txo_script.values: + address = ledger.hash160_to_address(txo_script.values.get('pubkey_hash', '')) + private_key = await ledger.get_private_key_for_address(wallet, address) + else: + private_key = next(iter(extra_keys.values())) assert private_key is not None, 'Cannot find private key for signing output.' tx = self._serialize_for_signature(i) txi.script.values['signature'] = \ diff --git a/tests/unit/wallet/test_transaction.py b/tests/unit/wallet/test_transaction.py index 5468b85b6..509c09ba7 100644 --- a/tests/unit/wallet/test_transaction.py +++ b/tests/unit/wallet/test_transaction.py @@ -262,6 +262,46 @@ class TestTransactionSerialization(unittest.TestCase): tx._reset() self.assertEqual(tx.raw, raw) + def test_redeem_scripthash_transaction(self): + raw = unhexlify( + "0200000001409223c2405238fdc516d4f2e8aa57637ce52d3b1ac42b26f1accdcda9697e79010000008a4" + "730440220033d5286f161da717d9d1bc3c2bc28da7636b38fc0c6aefb1e0864212f05282c02205df3ce13" + "5e79c76d44489212f77ad4e3a838562e601e6377704fa6206a6ae44f012102261773e7eebe9da80a5653d" + "865cc600362f8e7b2b598661139dd902b5b01ea101f03aaf30ab17576a914a3328f18ac1892a6667f713d" + "7020ff3437d973c888acfeffffff0180ed3e17000000001976a914353352b7ce1e3c9c05ffcd6ae97609d" + "e2999744488accdf50a00" + ) + tx = Transaction(raw) + self.assertEqual(tx.id, 'e466881128889d1cc4110627753051c22e72a81d11229a1a1337da06940bebcf') + self.assertEqual(tx.version, 2) + self.assertEqual(tx.locktime, 718285,) + self.assertEqual(len(tx.inputs), 1) + self.assertEqual(len(tx.outputs), 1) + + txin = tx.inputs[0] + self.assertEqual( + txin.txo_ref.id, + '797e69a9cdcdacf1262bc41a3b2de57c6357aae8f2d416c5fd385240c2239240:1' + ) + self.assertEqual(txin.txo_ref.position, 1) + self.assertEqual(txin.sequence, 4294967294) + self.assertIsNone(txin.coinbase) + self.assertEqual(txin.script.template.name, 'script_hash+timelock') + self.assertEqual( + hexlify(txin.script.values['signature']), + b'30440220033d5286f161da717d9d1bc3c2bc28da7636b38fc0c6aefb1e0864212f' + b'05282c02205df3ce135e79c76d44489212f77ad4e3a838562e601e6377704fa620' + b'6a6ae44f01' + ) + self.assertEqual( + hexlify(txin.script.values['pubkey']), + b'02261773e7eebe9da80a5653d865cc600362f8e7b2b598661139dd902b5b01ea10' + ) + script = txin.script.values['script'] + self.assertEqual(script.template.name, 'timelock') + self.assertEqual(script.values['height'], 717738) + self.assertEqual(hexlify(script.values['pubkey_hash']), b'a3328f18ac1892a6667f713d7020ff3437d973c8') + class TestTransactionSigning(AsyncioTestCase): From dd503fbb82537e7e2d97fb3f04b911124272cc8a Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 23 Mar 2022 00:11:40 -0400 Subject: [PATCH 4/7] set locktime from script --- lbry/wallet/script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lbry/wallet/script.py b/lbry/wallet/script.py index cfcf1e5da..5a39359be 100644 --- a/lbry/wallet/script.py +++ b/lbry/wallet/script.py @@ -408,6 +408,7 @@ class InputScript(Script): }) elif script_source: script = cls(source=script_source, template=cls.TIME_LOCK_SCRIPT) + script.parse(script.template) else: raise ValueError("script_source or both height and pubkey_hash are required.") return cls(template=cls.REDEEM_SCRIPT_HASH_TIME_LOCK, values={ From 16eb50a29114a3726d2e5c16e0c84bf0f5fcc9ef Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 4 Apr 2022 23:42:30 -0400 Subject: [PATCH 5/7] working jsonrpc_account_deposit --- lbry/wallet/script.py | 6 +++++- lbry/wallet/transaction.py | 15 +++++++++++---- .../blockchain/test_account_commands.py | 19 ++++++++++++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lbry/wallet/script.py b/lbry/wallet/script.py index 5a39359be..5f9869698 100644 --- a/lbry/wallet/script.py +++ b/lbry/wallet/script.py @@ -277,7 +277,7 @@ class Template: elif isinstance(opcode, PUSH_INTEGER): data = values[opcode.name] source.write_many(push_data( - data.to_bytes((data.bit_length() + 7) // 8, byteorder='little') + data.to_bytes((data.bit_length() + 8) // 8, byteorder='little', signed=True) )) elif isinstance(opcode, PUSH_SUBSCRIPT): data = values[opcode.name] @@ -417,6 +417,10 @@ class InputScript(Script): 'script': script }) + @property + def is_script_hash(self): + return self.template.name.startswith('script_hash+') + class OutputScript(Script): diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index fdc225a06..7b81661b9 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -718,8 +718,11 @@ class Transaction: stream.write_compact_size(len(self._inputs)) for i, txin in enumerate(self._inputs): if signing_input == i: - assert txin.txo_ref.txo is not None - txin.serialize_to(stream, txin.txo_ref.txo.script.source) + if txin.script.is_script_hash: + txin.serialize_to(stream, txin.script.values['script'].source) + else: + assert txin.txo_ref.txo is not None + txin.serialize_to(stream, txin.txo_ref.txo.script.source) else: txin.serialize_to(stream, b'') self._serialize_outputs(stream) @@ -949,9 +952,13 @@ class Transaction: return cls.create([], [payment, data], funding_accounts, change_account) @classmethod - def spend_time_lock(cls, time_locked_txo: Output, script: bytes, account: 'Account'): + async def spend_time_lock(cls, time_locked_txo: Output, script: bytes, account: 'Account'): txi = Input.spend_time_lock(time_locked_txo, script) - return cls.create([txi], [], [account], account, sign=False) + txi.sequence = 0xFFFFFFFE + tx = await cls.create([txi], [], [account], account, sign=False) + tx.locktime = txi.script.values['script'].values['height'] + tx._reset() + return tx @property def my_inputs(self): diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/blockchain/test_account_commands.py index ec5295118..27805c9d0 100644 --- a/tests/integration/blockchain/test_account_commands.py +++ b/tests/integration/blockchain/test_account_commands.py @@ -1,8 +1,10 @@ -from binascii import unhexlify +from binascii import hexlify, unhexlify from lbry.testcase import CommandTestCase +from lbry.wallet.script import InputScript from lbry.wallet.dewies import dewies_to_lbc from lbry.wallet.account import DeterministicChannelKeyManager +from lbry.crypto.hash import hash160 from lbry.crypto.base58 import Base58 @@ -293,15 +295,22 @@ class AccountManagement(CommandTestCase): async def test_time_locked_transactions(self): address = await self.account.receiving.get_or_create_usable_address() private_key = await self.ledger.get_private_key_for_address(self.wallet, address) - redeem = await self.blockchain.add_time_locked_address(210, address) + + script = InputScript( + template=InputScript.TIME_LOCK_SCRIPT, + values={'height': 210, 'pubkey_hash': self.ledger.address_to_hash160(address)} + ) + script_address = self.ledger.hash160_to_script_address(hash160(script.source)) + script_source = hexlify(script.source).decode() + await self.assertBalance(self.account, '10.0') - tx = await self.daemon.jsonrpc_account_send('4.0', redeem['address']) + tx = await self.daemon.jsonrpc_account_send('4.0', script_address) await self.confirm_tx(tx.id) await self.generate(510) await self.assertBalance(self.account, '5.999877') tx = await self.daemon.jsonrpc_account_deposit( - tx.id, 0, redeem['redeemScript'], + tx.id, 0, script_source, Base58.encode_check(self.ledger.private_key_to_wif(private_key.private_key_bytes)) ) await self.confirm_tx(tx.id) - await self.assertBalance(self.account, '9.999877') + await self.assertBalance(self.account, '9.9997545') From 8fb14bf7133447f0219bde21b8bad9276ce5bf7c Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Fri, 8 Apr 2022 09:59:22 -0400 Subject: [PATCH 6/7] remove command not available in lbcd --- lbry/wallet/orchstr8/node.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lbry/wallet/orchstr8/node.py b/lbry/wallet/orchstr8/node.py index 70a157b49..640a74cfb 100644 --- a/lbry/wallet/orchstr8/node.py +++ b/lbry/wallet/orchstr8/node.py @@ -679,11 +679,6 @@ class LBCWalletNode: def get_raw_transaction(self, txid): return self._cli_cmnd('getrawtransaction', txid, '1') - async def add_time_locked_address(self, height, address): - return json.loads( - await self._cli_cmnd('addtimelockedaddress', str(height), address) - ) - class HubProcess(asyncio.SubprocessProtocol): def __init__(self, ready, stopped): From 84ef52cf4d12a442a451bb1c557497b5a62856ae Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Fri, 8 Apr 2022 10:11:11 -0400 Subject: [PATCH 7/7] fix redeem scripthash test --- lbry/wallet/script.py | 6 +++--- tests/unit/wallet/test_script.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lbry/wallet/script.py b/lbry/wallet/script.py index 5f9869698..c70511ef5 100644 --- a/lbry/wallet/script.py +++ b/lbry/wallet/script.py @@ -362,8 +362,8 @@ class InputScript(Script): SMALL_INTEGER('signatures_count'), PUSH_MANY('pubkeys'), SMALL_INTEGER('pubkeys_count'), OP_CHECKMULTISIG )) - REDEEM_SCRIPT_HASH_MULTI_SIG = Template('script_hash+mult_sig', ( - PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey'), PUSH_SUBSCRIPT('script', MULTI_SIG_SCRIPT) + REDEEM_SCRIPT_HASH_MULTI_SIG = Template('script_hash+multi_sig', ( + OP_0, PUSH_MANY('signatures'), PUSH_SUBSCRIPT('script', MULTI_SIG_SCRIPT) )) TIME_LOCK_SCRIPT = Template('timelock', ( PUSH_INTEGER('height'), OP_CHECKLOCKTIMEVERIFY, OP_DROP, @@ -389,7 +389,7 @@ class InputScript(Script): }) @classmethod - def redeem_mult_sig_script_hash(cls, signatures, pubkeys): + def redeem_multi_sig_script_hash(cls, signatures, pubkeys): return cls(template=cls.REDEEM_SCRIPT_HASH_MULTI_SIG, values={ 'signatures': signatures, 'script': cls(template=cls.MULTI_SIG_SCRIPT, values={ diff --git a/tests/unit/wallet/test_script.py b/tests/unit/wallet/test_script.py index 7333e1133..6770002f1 100644 --- a/tests/unit/wallet/test_script.py +++ b/tests/unit/wallet/test_script.py @@ -130,12 +130,12 @@ class TestRedeemScriptHash(unittest.TestCase): def redeem_script_hash(self, sigs, pubkeys): # this checks that factory function correctly sets up the script - src1 = InputScript.redeem_script_hash( + src1 = InputScript.redeem_multi_sig_script_hash( [unhexlify(sig) for sig in sigs], [unhexlify(pubkey) for pubkey in pubkeys] ) subscript1 = src1.values['script'] - self.assertEqual(src1.template.name, 'script_hash') + self.assertEqual(src1.template.name, 'script_hash+multi_sig') self.assertListEqual([hexlify(v) for v in src1.values['signatures']], sigs) self.assertListEqual([hexlify(p) for p in subscript1.values['pubkeys']], pubkeys) self.assertEqual(subscript1.values['signatures_count'], len(sigs)) @@ -143,7 +143,7 @@ class TestRedeemScriptHash(unittest.TestCase): # now we test that it will round trip src2 = InputScript(src1.source) subscript2 = src2.values['script'] - self.assertEqual(src2.template.name, 'script_hash') + self.assertEqual(src2.template.name, 'script_hash+multi_sig') self.assertListEqual([hexlify(v) for v in src2.values['signatures']], sigs) self.assertListEqual([hexlify(p) for p in subscript2.values['pubkeys']], pubkeys) self.assertEqual(subscript2.values['signatures_count'], len(sigs))