diff --git a/pytest.ini b/pytest.ini index cce4ddc98f..0f83d26ca7 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,4 @@ [pytest] addopts = --pyargs trezorlib.tests.device_tests xfail_strict = true - -# List of markers that run as if not xfailed. See docs/testing.md for details -# run_xfail = stellar lisk nem +run_xfail = lisk diff --git a/src/apps/lisk/__init__.py b/src/apps/lisk/__init__.py new file mode 100644 index 0000000000..c82515e268 --- /dev/null +++ b/src/apps/lisk/__init__.py @@ -0,0 +1,24 @@ +from trezor.wire import register, protobuf_workflow +from trezor.messages.wire_types import \ + LiskGetAddress, LiskSignTx, LiskGetPublicKey + + +def dispatch_LiskGetAddress(*args, **kwargs): + from .get_address import layout_lisk_get_address + return layout_lisk_get_address(*args, **kwargs) + + +def dispatch_LiskGetPublicKey(*args, **kwargs): + from .get_public_key import lisk_get_public_key + return lisk_get_public_key(*args, **kwargs) + + +def dispatch_LiskSignTx(*args, **kwargs): + from .sign_tx import lisk_sign_tx + return lisk_sign_tx(*args, **kwargs) + + +def boot(): + register(LiskGetPublicKey, protobuf_workflow, dispatch_LiskGetPublicKey) + register(LiskGetAddress, protobuf_workflow, dispatch_LiskGetAddress) + register(LiskSignTx, protobuf_workflow, dispatch_LiskSignTx) diff --git a/src/apps/lisk/get_address.py b/src/apps/lisk/get_address.py new file mode 100644 index 0000000000..a273289d8b --- /dev/null +++ b/src/apps/lisk/get_address.py @@ -0,0 +1,23 @@ +from apps.common import seed +from apps.wallet.get_address import _show_address, _show_qr +from trezor.messages.LiskAddress import LiskAddress + +from .helpers import LISK_CURVE, get_address_from_public_key + + +async def layout_lisk_get_address(ctx, msg): + address_n = msg.address_n or () + + node = await seed.derive_node(ctx, address_n, LISK_CURVE) + pubkey = node.public_key() + pubkey = pubkey[1:] # skip ed25519 pubkey marker + address = get_address_from_public_key(pubkey) + + if msg.show_display: + while True: + if await _show_address(ctx, address): + break + if await _show_qr(ctx, address): + break + + return LiskAddress(address=address) diff --git a/src/apps/lisk/get_public_key.py b/src/apps/lisk/get_public_key.py new file mode 100644 index 0000000000..d79ec6fb1e --- /dev/null +++ b/src/apps/lisk/get_public_key.py @@ -0,0 +1,18 @@ +from apps.common import seed +from apps.wallet.get_public_key import _show_pubkey +from trezor.messages.LiskPublicKey import LiskPublicKey + +from .helpers import LISK_CURVE + + +async def lisk_get_public_key(ctx, msg): + address_n = msg.address_n or () + + node = await seed.derive_node(ctx, address_n, LISK_CURVE) + pubkey = node.public_key() + pubkey = pubkey[1:] # skip ed25519 pubkey marker + + if msg.show_display: + await _show_pubkey(ctx, pubkey) + + return LiskPublicKey(public_key=pubkey) diff --git a/src/apps/lisk/helpers.py b/src/apps/lisk/helpers.py new file mode 100644 index 0000000000..87aa46aaa5 --- /dev/null +++ b/src/apps/lisk/helpers.py @@ -0,0 +1,33 @@ +from trezor.crypto.hashlib import sha256 + +LISK_CURVE = 'ed25519' + + +def get_address_from_public_key(pubkey): + pubkeyhash = sha256(pubkey).digest() + address = int.from_bytes(pubkeyhash[:8], 'little') + return str(address) + 'L' + + +def get_votes_count(votes): + plus, minus = 0, 0 + for vote in votes: + if vote.startswith('+'): + plus += 1 + else: + minus += 1 + return plus, minus + + +def get_vote_tx_text(votes): + plus, minus = get_votes_count(votes) + text = [] + if plus > 0: + text.append(_text_with_plural('Add', plus)) + if minus > 0: + text.append(_text_with_plural('Remove', minus)) + return text + + +def _text_with_plural(txt, value): + return '%s %s %s' % (txt, value, ('votes' if value != 1 else 'vote')) diff --git a/src/apps/lisk/layout.py b/src/apps/lisk/layout.py new file mode 100644 index 0000000000..2847a4afd1 --- /dev/null +++ b/src/apps/lisk/layout.py @@ -0,0 +1,63 @@ +from apps.common.confirm import require_confirm, require_hold_to_confirm +from apps.wallet.get_public_key import _show_pubkey +from trezor import ui +from trezor.messages import ButtonRequestType +from trezor.ui.text import Text +from trezor.utils import chunks + +from .helpers import get_vote_tx_text + + +async def require_confirm_tx(ctx, to, value): + content = Text('Confirm sending', ui.ICON_SEND, + ui.BOLD, format_amount(value), + ui.NORMAL, 'to', + ui.MONO, *split_address(to), + icon_color=ui.GREEN) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_delegate_registration(ctx, delegate_name): + content = Text('Confirm transaction', ui.ICON_SEND, + 'Do you really want to', + 'register a delegate?', + ui.BOLD, *chunks(delegate_name, 20), + icon_color=ui.GREEN) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_vote_tx(ctx, votes): + content = Text('Confirm transaction', ui.ICON_SEND, + *get_vote_tx_text(votes), + icon_color=ui.GREEN) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_public_key(ctx, public_key): + return await _show_pubkey(ctx, public_key) + + +async def require_confirm_multisig(ctx, multisignature): + content = Text('Confirm transaction', ui.ICON_SEND, + ('Keys group length: %s' % len(multisignature.keys_group)), + ('Life time: %s' % multisignature.life_time), + ('Min: %s' % multisignature.min), + icon_color=ui.GREEN) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_fee(ctx, value, fee): + content = Text('Confirm transaction', ui.ICON_SEND, + ui.BOLD, format_amount(value), + ui.NORMAL, 'fee:', + ui.BOLD, format_amount(fee), + icon_color=ui.GREEN) + await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput) + + +def format_amount(value): + return '%s LSK' % (int(value) / 100000000) + + +def split_address(address): + return chunks(address, 16) diff --git a/src/apps/lisk/sign_tx.py b/src/apps/lisk/sign_tx.py new file mode 100644 index 0000000000..28a0167c72 --- /dev/null +++ b/src/apps/lisk/sign_tx.py @@ -0,0 +1,131 @@ +import ustruct +from apps.common import seed +from trezor import wire +from trezor.crypto.curve import ed25519 +from trezor.crypto.hashlib import sha256 +from trezor.messages import LiskTransactionType +from trezor.messages.LiskSignedTx import LiskSignedTx +from trezor.utils import HashWriter + +from . import layout +from .helpers import LISK_CURVE, get_address_from_public_key + + +async def lisk_sign_tx(ctx, msg): + pubkey, seckey = await _get_keys(ctx, msg) + transaction = _update_raw_tx(msg.transaction, pubkey) + + try: + await _require_confirm_by_type(ctx, transaction) + except AttributeError: + raise wire.DataError('The transaction has invalid asset data field') + + await layout.require_confirm_fee(ctx, transaction.amount, transaction.fee) + + txbytes = _get_transaction_bytes(transaction) + txhash = HashWriter(sha256) + for field in txbytes: + txhash.extend(field) + digest = txhash.get_digest() + + signature = ed25519.sign(seckey, digest) + + return LiskSignedTx(signature=signature) + + +async def _get_keys(ctx, msg): + address_n = msg.address_n or () + node = await seed.derive_node(ctx, address_n, LISK_CURVE) + + seckey = node.private_key() + pubkey = node.public_key() + pubkey = pubkey[1:] # skip ed25519 pubkey marker + + return pubkey, seckey + + +def _update_raw_tx(transaction, pubkey): + transaction.sender_public_key = pubkey + + # For CastVotes transactions, recipientId should be equal to transaction + # creator address. + if transaction.type == LiskTransactionType.CastVotes: + transaction.recipient_id = get_address_from_public_key(pubkey) + + return transaction + + +async def _require_confirm_by_type(ctx, transaction): + + if transaction.type == LiskTransactionType.Transfer: + return await layout.require_confirm_tx( + ctx, transaction.recipient_id, transaction.amount) + + if transaction.type == LiskTransactionType.RegisterDelegate: + return await layout.require_confirm_delegate_registration( + ctx, transaction.asset.delegate.username) + + if transaction.type == LiskTransactionType.CastVotes: + return await layout.require_confirm_vote_tx( + ctx, transaction.asset.votes) + + if transaction.type == LiskTransactionType.RegisterSecondPassphrase: + return await layout.require_confirm_public_key( + ctx, transaction.asset.signature.public_key) + + if transaction.type == LiskTransactionType.RegisterMultisignatureAccount: + return await layout.require_confirm_multisig( + ctx, transaction.asset.multisignature) + + raise wire.DataError('Invalid transaction type') + + +def _get_transaction_bytes(tx): + + # Required transaction parameters + t_type = ustruct.pack('Q', 0) + else: + # Lisk uses big-endian for recipient_id, string -> int -> bytes + t_recipient_id = ustruct.pack('>Q', int(tx.recipient_id[:-1])) + + t_amount = ustruct.pack('