Skip to content
This repository has been archived by the owner on May 28, 2019. It is now read-only.

Tezos integration #292

Merged
merged 7 commits into from Sep 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.cfg
Expand Up @@ -32,4 +32,4 @@ known_standard_library = micropython,ubinascii,ustruct,uctypes,utime,utimeq,trez
[tool:pytest]
addopts = --pyargs trezorlib.tests.device_tests
xfail_strict = true
run_xfail =
run_xfail = tezos
21 changes: 21 additions & 0 deletions src/apps/tezos/README.md
@@ -0,0 +1,21 @@
# Tezos

Tezos documentation can be found [here](http://tezos.gitlab.io).

## Operations

Tezos allows users to use multiple curves for private key derivation, but we support
only `ed25519` (because it is the most used one) where addresses are prefixed with `tz1`,
public keys with `edpk` and signatures with `edsig`. Other curves might be added later on.

Trezor supports basic Tezos user operations - reveal, transaction, origination, delegation.
When the account creates first operation in lifetime, reveal has to be bundled
with this operation to reveal account's public key.

#### Operations Explorer

[TzScan](http://tzscan.io)

## Tests

Unit tests are located in the `tests` directory, device tests are in the python-trezor repository.
8 changes: 8 additions & 0 deletions src/apps/tezos/__init__.py
@@ -0,0 +1,8 @@
from trezor import wire
from trezor.messages import MessageType


def boot():
wire.add(MessageType.TezosGetAddress, __name__, "get_address")
wire.add(MessageType.TezosSignTx, __name__, "sign_tx")
wire.add(MessageType.TezosGetPublicKey, __name__, "get_public_key")
28 changes: 28 additions & 0 deletions src/apps/tezos/get_address.py
@@ -0,0 +1,28 @@
from trezor.crypto import hashlib
from trezor.messages.TezosAddress import TezosAddress

from apps.common import seed
from apps.common.layout import show_address, show_qr
from apps.tezos.helpers import (
TEZOS_CURVE,
TEZOS_ED25519_ADDRESS_PREFIX,
base58_encode_check,
)


async def get_address(ctx, msg):
address_n = msg.address_n or ()
node = await seed.derive_node(ctx, address_n, TEZOS_CURVE)

pk = seed.remove_ed25519_prefix(node.public_key())
pkh = hashlib.blake2b(pk, outlen=20).digest()
address = base58_encode_check(pkh, prefix=TEZOS_ED25519_ADDRESS_PREFIX)

if msg.show_display:
while True:
if await show_address(ctx, address):
break
if await show_qr(ctx, address):
break

return TezosAddress(address=address)
29 changes: 29 additions & 0 deletions src/apps/tezos/get_public_key.py
@@ -0,0 +1,29 @@
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.messages.TezosPublicKey import TezosPublicKey
from trezor.ui.text import Text
from trezor.utils import chunks

from apps.common import seed
from apps.common.confirm import require_confirm
from apps.tezos.helpers import TEZOS_CURVE, TEZOS_PUBLICKEY_PREFIX, base58_encode_check


async def get_public_key(ctx, msg):
address_n = msg.address_n or ()
node = await seed.derive_node(ctx, address_n, TEZOS_CURVE)

pk = seed.remove_ed25519_prefix(node.public_key())
pk_prefixed = base58_encode_check(pk, prefix=TEZOS_PUBLICKEY_PREFIX)

if msg.show_display:
await _show_tezos_pubkey(ctx, pk_prefixed)

return TezosPublicKey(public_key=pk_prefixed)


async def _show_tezos_pubkey(ctx, pubkey):
lines = chunks(pubkey, 18)
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
text.mono(*lines)
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)
37 changes: 37 additions & 0 deletions src/apps/tezos/helpers.py
@@ -0,0 +1,37 @@
from micropython import const

from trezor.crypto import base58

TEZOS_CURVE = "ed25519"
TEZOS_AMOUNT_DIVISIBILITY = const(6)
TEZOS_ED25519_ADDRESS_PREFIX = "tz1"
TEZOS_ORIGINATED_ADDRESS_PREFIX = "KT1"
TEZOS_PUBLICKEY_PREFIX = "edpk"
TEZOS_SIGNATURE_PREFIX = "edsig"
TEZOS_PREFIX_BYTES = {
# addresses
"tz1": [6, 161, 159],
"tz2": [6, 161, 161],
"tz3": [6, 161, 164],
"KT1": [2, 90, 121],
# public keys
"edpk": [13, 15, 37, 217],
# signatures
"edsig": [9, 245, 205, 134, 18],
# operation hash
"o": [5, 116],
}


def base58_encode_check(payload, prefix=None):
result = payload
if prefix is not None:
result = bytes(TEZOS_PREFIX_BYTES[prefix]) + payload
return base58.encode_check(result)


def base58_decode_check(enc, prefix=None):
decoded = base58.decode_check(enc)
if prefix is not None:
decoded = decoded[len(TEZOS_PREFIX_BYTES[prefix]) :]
return decoded
71 changes: 71 additions & 0 deletions src/apps/tezos/layout.py
@@ -0,0 +1,71 @@
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.text import Text
from trezor.utils import chunks, format_amount

from apps.common.confirm import *
from apps.tezos.helpers import TEZOS_AMOUNT_DIVISIBILITY


async def require_confirm_tx(ctx, to, value):
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
text.bold(format_tezos_amount(value))
text.normal("to")
text.mono(*split_address(to))
return await require_confirm(ctx, text, ButtonRequestType.SignTx)


async def require_confirm_fee(ctx, value, fee):
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text.normal("Amount:")
text.bold(format_tezos_amount(value))
text.normal("Fee:")
text.bold(format_tezos_amount(fee))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)


async def require_confirm_origination(ctx, address):
text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE)
text.normal("Address:")
text.mono(*split_address(address))
return await require_confirm(ctx, text, ButtonRequestType.SignTx)


async def require_confirm_origination_fee(ctx, balance, fee):
text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE)
text.normal("Balance:")
text.bold(format_tezos_amount(balance))
text.normal("Fee:")
text.bold(format_tezos_amount(fee))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)


async def require_confirm_delegation_baker(ctx, baker):
text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE)
text.normal("Baker address:")
text.mono(*split_address(baker))
return await require_confirm(ctx, text, ButtonRequestType.SignTx)


async def require_confirm_set_delegate(ctx, fee):
text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE)
text.normal("Fee:")
text.bold(format_tezos_amount(fee))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)


async def require_confirm_register_delegate(ctx, address, fee):
text = Text("Register delegate", ui.ICON_SEND, icon_color=ui.BLUE)
text.bold("Fee: " + format_tezos_amount(fee))
text.normal("Address:")
text.mono(*split_address(address))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)


def split_address(address):
return chunks(address, 18)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use common.layout.split_address. It has size=17, but that doesn't matter that much or will be refactored together later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I use 17, the address in transaction won't fit on display.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, keep it.



def format_tezos_amount(value):
formatted_value = format_amount(value, TEZOS_AMOUNT_DIVISIBILITY)
return formatted_value + " XTZ"
164 changes: 164 additions & 0 deletions src/apps/tezos/sign_tx.py
@@ -0,0 +1,164 @@
from trezor import wire
from trezor.crypto import hashlib
from trezor.crypto.curve import ed25519
from trezor.messages import TezosContractType
from trezor.messages.TezosSignedTx import TezosSignedTx

from apps.common import seed
from apps.common.writers import write_bytes, write_uint8
from apps.tezos.helpers import (
TEZOS_CURVE,
TEZOS_ORIGINATED_ADDRESS_PREFIX,
TEZOS_SIGNATURE_PREFIX,
base58_encode_check,
)
from apps.tezos.layout import *


async def sign_tx(ctx, msg):
address_n = msg.address_n or ()
node = await seed.derive_node(ctx, address_n, TEZOS_CURVE)

if msg.transaction is not None:
to = _get_address_from_contract(msg.transaction.destination)
await require_confirm_tx(ctx, to, msg.transaction.amount)
await require_confirm_fee(ctx, msg.transaction.amount, msg.transaction.fee)

elif msg.origination is not None:
source = _get_address_from_contract(msg.origination.source)
await require_confirm_origination(ctx, source)
await require_confirm_origination_fee(
ctx, msg.origination.balance, msg.origination.fee
)

elif msg.delegation is not None:
source = _get_address_from_contract(msg.delegation.source)

delegate = None
if msg.delegation.delegate is not None:
delegate = _get_address_by_tag(msg.delegation.delegate)

if delegate is not None and source != delegate:
await require_confirm_delegation_baker(ctx, delegate)
await require_confirm_set_delegate(ctx, msg.delegation.fee)
# if account registers itself as a delegate
else:
await require_confirm_register_delegate(ctx, source, msg.delegation.fee)

else:
raise wire.DataError("Invalid operation")

w = bytearray()
_get_operation_bytes(w, msg)

opbytes = bytes(w)

# watermark 0x03 is prefix for transactions, delegations, originations, reveals...
watermark = bytes([3])
wm_opbytes = watermark + opbytes
wm_opbytes_hash = hashlib.blake2b(wm_opbytes, outlen=32).digest()

signature = ed25519.sign(node.private_key(), wm_opbytes_hash)

sig_op_contents = opbytes + signature
sig_op_contents_hash = hashlib.blake2b(sig_op_contents, outlen=32).digest()
ophash = base58_encode_check(sig_op_contents_hash, prefix="o")

sig_prefixed = base58_encode_check(signature, prefix=TEZOS_SIGNATURE_PREFIX)

return TezosSignedTx(
signature=sig_prefixed, sig_op_contents=sig_op_contents, operation_hash=ophash
)


def _get_address_by_tag(address_hash):
prefixes = ["tz1", "tz2", "tz3"]
tag = int(address_hash[0])

if 0 <= tag < len(prefixes):
return base58_encode_check(address_hash[1:], prefix=prefixes[tag])
raise wire.DataError("Invalid tag in address hash")


def _get_address_from_contract(address):
if address.tag == TezosContractType.Implicit:
return _get_address_by_tag(address.hash)

elif address.tag == TezosContractType.Originated:
return base58_encode_check(
address.hash[:-1], prefix=TEZOS_ORIGINATED_ADDRESS_PREFIX
)

raise wire.DataError("Invalid contract type")


def _get_operation_bytes(w: bytearray, msg):
write_bytes(w, msg.branch)

# when the account sends first operation in lifetime,
# we need to reveal its publickey
if msg.reveal is not None:
_encode_common(w, msg.reveal, "reveal")
write_bytes(w, msg.reveal.public_key)

# transaction operation
if msg.transaction is not None:
_encode_common(w, msg.transaction, "transaction")
_encode_zarith(w, msg.transaction.amount)
_encode_contract_id(w, msg.transaction.destination)
_encode_data_with_bool_prefix(w, msg.transaction.parameters)
# origination operation
elif msg.origination is not None:
_encode_common(w, msg.origination, "origination")
write_bytes(w, msg.origination.manager_pubkey)
_encode_zarith(w, msg.origination.balance)
_encode_bool(w, msg.origination.spendable)
_encode_bool(w, msg.origination.delegatable)
_encode_data_with_bool_prefix(w, msg.origination.delegate)
_encode_data_with_bool_prefix(w, msg.origination.script)
# delegation operation
elif msg.delegation is not None:
_encode_common(w, msg.delegation, "delegation")
_encode_data_with_bool_prefix(w, msg.delegation.delegate)


def _encode_common(w: bytearray, operation, str_operation):
operation_tags = {"reveal": 7, "transaction": 8, "origination": 9, "delegation": 10}
write_uint8(w, operation_tags[str_operation])
_encode_contract_id(w, operation.source)
_encode_zarith(w, operation.fee)
_encode_zarith(w, operation.counter)
_encode_zarith(w, operation.gas_limit)
_encode_zarith(w, operation.storage_limit)


def _encode_contract_id(w: bytearray, contract_id):
write_uint8(w, contract_id.tag)
write_bytes(w, contract_id.hash)


def _encode_bool(w: bytearray, boolean):
if boolean:
write_uint8(w, 255)
else:
write_uint8(w, 0)


def _encode_data_with_bool_prefix(w: bytearray, data):
if data:
_encode_bool(w, True)
write_bytes(w, data)
else:
_encode_bool(w, False)


def _encode_zarith(w: bytearray, num):
while True:
byte = num & 127
num = num >> 7

if num == 0:
write_uint8(w, byte)
break

write_uint8(w, 128 | byte)
2 changes: 2 additions & 0 deletions src/main.py
Expand Up @@ -18,6 +18,7 @@
import apps.stellar
import apps.ripple
import apps.cardano
import apps.tezos

if __debug__:
import apps.debug
Expand All @@ -34,6 +35,7 @@
apps.stellar.boot()
apps.ripple.boot()
apps.cardano.boot()
apps.tezos.boot()
if __debug__:
apps.debug.boot()
else:
Expand Down