added support for signed supports

This commit is contained in:
Lex Berezhny 2020-06-05 15:19:14 -04:00
parent 07f7a77ac0
commit c03e30a01f
7 changed files with 154 additions and 49 deletions

View file

@ -3980,7 +3980,9 @@ class Daemon(metaclass=JSONRPCServerType):
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_support_create( async def jsonrpc_support_create(
self, claim_id, amount, tip=False, account_id=None, wallet_id=None, funding_account_ids=None, self, claim_id, amount, tip=False,
channel_id=None, channel_name=None, channel_account_id=None,
account_id=None, wallet_id=None, funding_account_ids=None,
preview=False, blocking=False): preview=False, blocking=False):
""" """
Create a support or a tip for name claim. Create a support or a tip for name claim.
@ -3988,12 +3990,18 @@ class Daemon(metaclass=JSONRPCServerType):
Usage: Usage:
support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>) support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>)
[--tip] [--account_id=<account_id>] [--wallet_id=<wallet_id>] [--tip] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
[--channel_id=<channel_id> | --channel_name=<channel_name>]
[--channel_account_id=<channel_account_id>...]
[--preview] [--blocking] [--funding_account_ids=<funding_account_ids>...] [--preview] [--blocking] [--funding_account_ids=<funding_account_ids>...]
Options: Options:
--claim_id=<claim_id> : (str) claim_id of the claim to support --claim_id=<claim_id> : (str) claim_id of the claim to support
--amount=<amount> : (decimal) amount of support --amount=<amount> : (decimal) amount of support
--tip : (bool) send support to claim owner, default: false. --tip : (bool) send support to claim owner, default: false.
--channel_id=<channel_id> : (str) claim id of the supporters identity channel
--channel_name=<channel_name> : (str) name of the supporters identity channel
--channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
for channel certificates, defaults to all accounts.
--account_id=<account_id> : (str) account to use for holding the transaction --account_id=<account_id> : (str) account to use for holding the transaction
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet --wallet_id=<wallet_id> : (str) restrict operation to specific wallet
--funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
@ -4005,6 +4013,7 @@ class Daemon(metaclass=JSONRPCServerType):
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first." assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
funding_accounts = wallet.get_accounts_or_all(funding_account_ids) funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)
amount = self.get_dewies_or_error("amount", amount) amount = self.get_dewies_or_error("amount", amount)
claim = await self.ledger.get_claim_by_claim_id(wallet.accounts, claim_id) claim = await self.ledger.get_claim_by_claim_id(wallet.accounts, claim_id)
claim_address = claim.get_address(self.ledger) claim_address = claim.get_address(self.ledger)
@ -4013,8 +4022,13 @@ class Daemon(metaclass=JSONRPCServerType):
claim_address = await account.receiving.get_or_create_usable_address() claim_address = await account.receiving.get_or_create_usable_address()
tx = await Transaction.support( tx = await Transaction.support(
claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0] claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0], channel
) )
new_txo = tx.outputs[0]
if channel:
new_txo.sign(channel)
await tx.sign(funding_accounts)
if not preview: if not preview:
await self.broadcast_or_release(tx, blocking) await self.broadcast_or_release(tx, blocking)

View file

@ -7,6 +7,7 @@ from json import JSONEncoder
from google.protobuf.message import DecodeError from google.protobuf.message import DecodeError
from lbry.schema.claim import Claim from lbry.schema.claim import Claim
from lbry.schema.support import Support
from lbry.torrent.torrent_manager import TorrentSource from lbry.torrent.torrent_manager import TorrentSource
from lbry.wallet import Wallet, Ledger, Account, Transaction, Output from lbry.wallet import Wallet, Ledger, Account, Transaction, Output
from lbry.wallet.bip32 import PubKey from lbry.wallet.bip32 import PubKey
@ -135,6 +136,8 @@ class JSONResponseEncoder(JSONEncoder):
return self.encode_output(obj) return self.encode_output(obj)
if isinstance(obj, Claim): if isinstance(obj, Claim):
return self.encode_claim(obj) return self.encode_claim(obj)
if isinstance(obj, Support):
return obj.to_dict()
if isinstance(obj, PubKey): if isinstance(obj, PubKey):
return obj.extended_key_string() return obj.extended_key_string()
if isinstance(obj, datetime): if isinstance(obj, datetime):
@ -220,22 +223,25 @@ class JSONResponseEncoder(JSONEncoder):
output['claims'] = [self.encode_output(o) for o in txo.claims] output['claims'] = [self.encode_output(o) for o in txo.claims]
if txo.reposted_claim is not None: if txo.reposted_claim is not None:
output['reposted_claim'] = self.encode_output(txo.reposted_claim) output['reposted_claim'] = self.encode_output(txo.reposted_claim)
if txo.script.is_claim_name or txo.script.is_update_claim: if txo.script.is_claim_name or txo.script.is_update_claim or txo.script.is_support_claim_data:
try: try:
output['value'] = txo.claim output['value'] = txo.signable
output['value_type'] = txo.claim.claim_type
if self.include_protobuf: if self.include_protobuf:
output['protobuf'] = hexlify(txo.claim.to_bytes()) output['protobuf'] = hexlify(txo.signable.to_bytes())
if txo.purchase_receipt is not None: if txo.purchase_receipt is not None:
output['purchase_receipt'] = self.encode_output(txo.purchase_receipt) output['purchase_receipt'] = self.encode_output(txo.purchase_receipt)
if txo.script.is_claim_name or txo.script.is_update_claim:
output['value_type'] = txo.claim.claim_type
if txo.claim.is_channel: if txo.claim.is_channel:
output['has_signing_key'] = txo.has_private_key output['has_signing_key'] = txo.has_private_key
if check_signature and txo.claim.is_signed: elif txo.script.is_support_claim_data:
output['value_type'] = 'emoji'
if check_signature and txo.signable.is_signed:
if txo.channel is not None: if txo.channel is not None:
output['signing_channel'] = self.encode_output(txo.channel) output['signing_channel'] = self.encode_output(txo.channel)
output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger) output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger)
else: else:
output['signing_channel'] = {'channel_id': txo.claim.signing_channel_id} output['signing_channel'] = {'channel_id': txo.signable.signing_channel_id}
output['is_channel_signature_valid'] = False output['is_channel_signature_valid'] = False
except DecodeError: except DecodeError:
pass pass

View file

@ -30,14 +30,10 @@ class Claim(Signable):
COLLECTION = 'collection' COLLECTION = 'collection'
REPOST = 'repost' REPOST = 'repost'
__slots__ = 'version', __slots__ = ()
message_class = ClaimMessage message_class = ClaimMessage
def __init__(self, message=None):
super().__init__(message)
self.version = 2
@property @property
def claim_type(self) -> str: def claim_type(self) -> str:
return self.message.WhichOneof('type') return self.message.WhichOneof('type')

View file

@ -1,6 +1,15 @@
from lbry.schema.base import Signable from lbry.schema.base import Signable
from lbry.schema.types.v2.support_pb2 import Support as SupportMessage
class Support(Signable): class Support(Signable):
__slots__ = () __slots__ = ()
message_class = None # TODO: add support protobufs message_class = SupportMessage
@property
def emoji(self) -> str:
return self.message.emoji
@emoji.setter
def emoji(self, emoji: str):
self.message.emoji = emoji

View file

@ -438,6 +438,17 @@ class OutputScript(Script):
SUPPORT_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes SUPPORT_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
)) ))
SUPPORT_CLAIM_DATA_OPCODES = (
OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('support'),
OP_2DROP, OP_2DROP
)
SUPPORT_CLAIM_DATA_PUBKEY = Template('support_claim+data+pay_pubkey_hash', (
SUPPORT_CLAIM_DATA_OPCODES + PAY_PUBKEY_HASH.opcodes
))
SUPPORT_CLAIM_DATA_SCRIPT = Template('support_claim+data+pay_script_hash', (
SUPPORT_CLAIM_DATA_OPCODES + PAY_SCRIPT_HASH.opcodes
))
UPDATE_CLAIM_OPCODES = ( UPDATE_CLAIM_OPCODES = (
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'), OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
OP_2DROP, OP_2DROP OP_2DROP, OP_2DROP
@ -474,6 +485,8 @@ class OutputScript(Script):
CLAIM_NAME_SCRIPT, CLAIM_NAME_SCRIPT,
SUPPORT_CLAIM_PUBKEY, SUPPORT_CLAIM_PUBKEY,
SUPPORT_CLAIM_SCRIPT, SUPPORT_CLAIM_SCRIPT,
SUPPORT_CLAIM_DATA_PUBKEY,
SUPPORT_CLAIM_DATA_SCRIPT,
UPDATE_CLAIM_PUBKEY, UPDATE_CLAIM_PUBKEY,
UPDATE_CLAIM_SCRIPT, UPDATE_CLAIM_SCRIPT,
SELL_CLAIM, SELL_SCRIPT, SELL_CLAIM, SELL_SCRIPT,
@ -527,6 +540,16 @@ class OutputScript(Script):
'pubkey_hash': pubkey_hash 'pubkey_hash': pubkey_hash
}) })
@classmethod
def pay_support_data_pubkey_hash(
cls, claim_name: bytes, claim_id: bytes, support, pubkey_hash: bytes):
return cls(template=cls.SUPPORT_CLAIM_DATA_PUBKEY, values={
'claim_name': claim_name,
'claim_id': claim_id,
'support': support,
'pubkey_hash': pubkey_hash
})
@classmethod @classmethod
def sell_script(cls, price): def sell_script(cls, price):
return cls(template=cls.SELL_SCRIPT, values={ return cls(template=cls.SELL_SCRIPT, values={
@ -575,6 +598,10 @@ class OutputScript(Script):
def is_support_claim(self): def is_support_claim(self):
return self.template.name.startswith('support_claim+') return self.template.name.startswith('support_claim+')
@property
def is_support_claim_data(self):
return self.template.name.startswith('support_claim+data+')
@property @property
def is_sell_claim(self): def is_sell_claim(self):
return self.template.name.startswith('sell_claim+') return self.template.name.startswith('sell_claim+')

View file

@ -19,7 +19,9 @@ from lbry.crypto.hash import hash160, sha256
from lbry.crypto.base58 import Base58 from lbry.crypto.base58 import Base58
from lbry.schema.url import normalize_name from lbry.schema.url import normalize_name
from lbry.schema.claim import Claim from lbry.schema.claim import Claim
from lbry.schema.base import Signable
from lbry.schema.purchase import Purchase from lbry.schema.purchase import Purchase
from lbry.schema.support import Support
from .script import InputScript, OutputScript from .script import InputScript, OutputScript
from .constants import COIN, NULL_HASH32 from .constants import COIN, NULL_HASH32
@ -211,7 +213,7 @@ class Output(InputOutput):
'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input', 'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input',
'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips', 'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips',
'purchase', 'purchased_claim', 'purchase_receipt', 'purchase', 'purchased_claim', 'purchase_receipt',
'reposted_claim', 'claims', 'reposted_claim', 'claims', '_signable'
) )
def __init__(self, amount: int, script: OutputScript, def __init__(self, amount: int, script: OutputScript,
@ -239,6 +241,7 @@ class Output(InputOutput):
self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim
self.reposted_claim: 'Output' = None # txo representing claim being reposted self.reposted_claim: 'Output' = None # txo representing claim being reposted
self.claims: List['Output'] = None # resolved claims for collection self.claims: List['Output'] = None # resolved claims for collection
self._signable: Optional[Signable] = None
self.meta = {} self.meta = {}
def update_annotations(self, annotated: 'Output'): def update_annotations(self, annotated: 'Output'):
@ -312,6 +315,10 @@ class Output(InputOutput):
def is_support(self) -> bool: def is_support(self) -> bool:
return self.script.is_support_claim return self.script.is_support_claim
@property
def is_support_data(self) -> bool:
return self.script.is_support_claim_data
@property @property
def claim_hash(self) -> bytes: def claim_hash(self) -> bytes:
if self.script.is_claim_name: if self.script.is_claim_name:
@ -347,9 +354,33 @@ class Output(InputOutput):
def can_decode_claim(self): def can_decode_claim(self):
try: try:
return self.claim return self.claim
except: # pylint: disable=bare-except except Exception:
return False return False
@property
def support(self) -> Support:
if self.is_support_data:
if not isinstance(self.script.values['support'], Support):
self.script.values['support'] = Support.from_bytes(self.script.values['support'])
return self.script.values['support']
raise ValueError('Only supports with data can be represented as Supports.')
@property
def can_decode_support(self):
try:
return self.support
except Exception:
return False
@property
def signable(self) -> Signable:
if self._signable is None:
if self.is_claim:
self._signable = self.claim
elif self.is_support_data:
self._signable = self.support
return self._signable
@property @property
def permanent_url(self) -> str: def permanent_url(self) -> str:
if self.script.is_claim_involved: if self.script.is_claim_involved:
@ -361,22 +392,22 @@ class Output(InputOutput):
return self.private_key is not None return self.private_key is not None
def get_signature_digest(self, ledger): def get_signature_digest(self, ledger):
if self.claim.unsigned_payload: if self.signable.unsigned_payload:
pieces = [ pieces = [
Base58.decode(self.get_address(ledger)), Base58.decode(self.get_address(ledger)),
self.claim.unsigned_payload, self.signable.unsigned_payload,
self.claim.signing_channel_hash[::-1] self.signable.signing_channel_hash[::-1]
] ]
else: else:
pieces = [ pieces = [
self.tx_ref.tx.inputs[0].txo_ref.hash, self.tx_ref.tx.inputs[0].txo_ref.hash,
self.claim.signing_channel_hash, self.signable.signing_channel_hash,
self.claim.to_message_bytes() self.signable.to_message_bytes()
] ]
return sha256(b''.join(pieces)) return sha256(b''.join(pieces))
def get_encoded_signature(self): def get_encoded_signature(self):
signature = hexlify(self.claim.signature) signature = hexlify(self.signable.signature)
r = int(signature[:int(len(signature)/2)], 16) r = int(signature[:int(len(signature)/2)], 16)
s = 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) return ecdsa.util.sigencode_der(r, s, len(signature)*4)
@ -400,18 +431,18 @@ class Output(InputOutput):
def sign(self, channel: 'Output', first_input_id=None): def sign(self, channel: 'Output', first_input_id=None):
self.channel = channel self.channel = channel
self.claim.signing_channel_hash = channel.claim_hash self.signable.signing_channel_hash = channel.claim_hash
digest = sha256(b''.join([ digest = sha256(b''.join([
first_input_id or self.tx_ref.tx.inputs[0].txo_ref.hash, first_input_id or self.tx_ref.tx.inputs[0].txo_ref.hash,
self.claim.signing_channel_hash, self.signable.signing_channel_hash,
self.claim.to_message_bytes() self.signable.to_message_bytes()
])) ]))
self.claim.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) self.signable.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
self.script.generate() self.script.generate()
def clear_signature(self): def clear_signature(self):
self.channel = None self.channel = None
self.claim.clear_signature() self.signable.clear_signature()
async def generate_channel_private_key(self): async def generate_channel_private_key(self):
self.private_key = await asyncio.get_event_loop().run_in_executor( self.private_key = await asyncio.get_event_loop().run_in_executor(
@ -446,6 +477,14 @@ class Output(InputOutput):
) )
return cls(amount, script) return cls(amount, script)
@classmethod
def pay_support_data_pubkey_hash(
cls, amount: int, claim_name: str, claim_id: str, support: Support, pubkey_hash: bytes) -> 'Output':
script = OutputScript.pay_support_data_pubkey_hash(
claim_name.encode(), unhexlify(claim_id)[::-1], support, pubkey_hash
)
return cls(amount, script)
@classmethod @classmethod
def add_purchase_data(cls, purchase: Purchase) -> 'Output': def add_purchase_data(cls, purchase: Purchase) -> 'Output':
script = OutputScript.return_data(purchase) script = OutputScript.return_data(purchase)
@ -860,12 +899,20 @@ class Transaction:
@classmethod @classmethod
def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str, def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str,
funding_accounts: List['Account'], change_account: 'Account'): funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None):
ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account) ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)
if signing_channel is not None:
support = Support()
support.emoji = '👍'
support_output = Output.pay_support_data_pubkey_hash(
amount, claim_name, claim_id, support, ledger.address_to_hash160(holding_address)
)
support_output.sign(signing_channel, b'placeholder txid:nout')
else:
support_output = Output.pay_support_pubkey_hash( support_output = Output.pay_support_pubkey_hash(
amount, claim_name, claim_id, ledger.address_to_hash160(holding_address) amount, claim_name, claim_id, ledger.address_to_hash160(holding_address)
) )
return cls.create([], [support_output], funding_accounts, change_account) return cls.create([], [support_output], funding_accounts, change_account, sign=False)
@classmethod @classmethod
def purchase(cls, claim_id: str, amount: int, merchant_address: bytes, def purchase(cls, claim_id: str, amount: int, merchant_address: bytes,

View file

@ -1886,6 +1886,12 @@ class SupportCommands(CommandTestCase):
self.assertTrue(txs[1]['support_info'][0]['is_tip']) self.assertTrue(txs[1]['support_info'][0]['is_tip'])
self.assertTrue(txs[1]['support_info'][0]['is_spent']) self.assertTrue(txs[1]['support_info'][0]['is_spent'])
async def test_signed_supports(self):
channel_id = self.get_claim_id(await self.channel_create())
stream_id = self.get_claim_id(await self.stream_create())
tx = await self.support_create(stream_id, '0.3', channel_id=channel_id)
self.assertTrue(tx['outputs'][0]['is_channel_signature_valid'])
class CollectionCommands(CommandTestCase): class CollectionCommands(CommandTestCase):