Skip to content

Commit

Permalink
Merge 84ef52c into 8cdcd77
Browse files Browse the repository at this point in the history
  • Loading branch information
eukreign committed Apr 8, 2022
2 parents 8cdcd77 + 84ef52c commit 6b9b43f
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 77 deletions.
43 changes: 43 additions & 0 deletions lbry/extras/daemon/daemon.py
Expand Up @@ -28,6 +28,7 @@
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
Expand Down Expand Up @@ -1872,6 +1873,48 @@ def jsonrpc_account_fund(self, to_account=None, from_account=None, amount='0.0',
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 <txid> <nout> <redeem_script> <private_key>
[<to_account> | --to_account=<to_account>]
[--wallet_id=<wallet_id>] [--preview] [--blocking]
Options:
--txid=<txid> : (str) id of the transaction
--nout=<nout> : (int) output number in the transaction
--redeem_script=<redeem_script> : (str) redeem script for output
--private_key=<private_key> : (str) private key to sign transaction
--to_account=<to_account> : (str) deposit to this account
--wallet_id=<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]
)
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())
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):
"""
Expand Down
4 changes: 4 additions & 0 deletions lbry/wallet/bip32.py
Expand Up @@ -215,6 +215,10 @@ def from_pem(cls, ledger, pem) -> 'PrivateKey':
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). """
Expand Down
109 changes: 42 additions & 67 deletions lbry/wallet/script.py
Expand Up @@ -17,6 +17,7 @@
OP_EQUALVERIFY = 0x88
OP_CHECKSIG = 0xac
OP_CHECKMULTISIG = 0xae
OP_CHECKLOCKTIMEVERIFY = 0xb1
OP_EQUAL = 0x87
OP_PUSHDATA1 = 0x4c
OP_PUSHDATA2 = 0x4d
Expand Down Expand Up @@ -276,7 +277,7 @@ def generate(self, values):
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]
Expand Down Expand Up @@ -357,19 +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+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,
# 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_SCRIPT_HASH_TIME_LOCK,
REDEEM_SCRIPT_HASH_MULTI_SIG,
]

@classmethod
Expand All @@ -380,20 +389,38 @@ def redeem_pubkey_hash(cls, signature, pubkey):
})

@classmethod
def redeem_script_hash(cls, signatures, pubkeys):
return cls(template=cls.REDEEM_SCRIPT_HASH, values={
def redeem_multi_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)
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={
'signature': signature,
'pubkey': pubkey,
'script': script
})

@property
def is_script_hash(self):
return self.template.name.startswith('script_hash+')


class OutputScript(Script):

Expand Down Expand Up @@ -460,21 +487,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,
Expand All @@ -489,8 +501,6 @@ class OutputScript(Script):
SUPPORT_CLAIM_DATA_SCRIPT,
UPDATE_CLAIM_PUBKEY,
UPDATE_CLAIM_SCRIPT,
SELL_CLAIM, SELL_SCRIPT,
BUY_CLAIM,
]

@classmethod
Expand Down Expand Up @@ -550,30 +560,6 @@ def pay_support_data_pubkey_hash(
'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')
Expand Down Expand Up @@ -602,17 +588,6 @@ def is_support_claim(self):
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))
35 changes: 29 additions & 6 deletions lbry/wallet/transaction.py
Expand Up @@ -145,6 +145,14 @@ def spend(cls, txo: 'Output') -> 'Input':
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_script_hash(
cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY, script_source=script_source
)
return cls(txo.ref, script)

@property
def amount(self) -> int:
""" Amount this input adds to the transaction. """
Expand Down Expand Up @@ -710,8 +718,11 @@ def _serialize_for_signature(self, signing_input: int) -> bytes:
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)
Expand Down Expand Up @@ -854,16 +865,19 @@ async def create(cls, inputs: Iterable[Input], outputs: Iterable[Output],
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):
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 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'] = \
Expand Down Expand Up @@ -937,6 +951,15 @@ def purchase(cls, claim_id: str, amount: int, merchant_address: bytes,
data = Output.add_purchase_data(Purchase(claim_id))
return cls.create([], [payment, data], funding_accounts, change_account)

@classmethod
async def spend_time_lock(cls, time_locked_txo: Output, script: bytes, account: 'Account'):
txi = Input.spend_time_lock(time_locked_txo, script)
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):
for txi in self.inputs:
Expand Down
27 changes: 26 additions & 1 deletion tests/integration/blockchain/test_account_commands.py
@@ -1,8 +1,11 @@
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


def extract(d, keys):
Expand Down Expand Up @@ -289,3 +292,25 @@ async def test_deterministic_channel_keys(self):
self.assertTrue(channel2c.has_private_key)
self.assertTrue(channel3c.has_private_key)

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)

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', 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, 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.9997545')
6 changes: 3 additions & 3 deletions tests/unit/wallet/test_script.py
Expand Up @@ -130,20 +130,20 @@ 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))
self.assertEqual(subscript1.values['pubkeys_count'], len(pubkeys))
# 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))
Expand Down

0 comments on commit 6b9b43f

Please sign in to comment.