1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-21 23:18:13 +00:00

Tezos: Add voting support (#41)

Tezos: Add voting support
This commit is contained in:
Tomas Susanka 2019-04-23 14:00:32 +02:00 committed by GitHub
commit c9f380eae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 395 additions and 26 deletions

View File

@ -55,6 +55,8 @@ message TezosSignTx {
optional TezosTransactionOp transaction = 4; // Tezos transaction operation
optional TezosOriginationOp origination = 5; // Tezos origination operation
optional TezosDelegationOp delegation = 6; // Tezos delegation operation
optional TezosProposalOp proposal = 7; // Tezos proposal operation
optional TezosBallotOp ballot = 8; // Tezos ballot operation
/*
* Tezos contract ID
*/
@ -120,6 +122,29 @@ message TezosSignTx {
optional uint64 storage_limit = 5;
optional bytes delegate = 6;
}
/**
* Structure representing information for proposal
*/
message TezosProposalOp {
optional bytes source = 1; //Contains only public_key_hash, not to be confused with TezosContractID
optional uint64 period = 2;
repeated bytes proposals = 4;
}
/**
* Structure representing information for ballot
*/
message TezosBallotOp {
optional bytes source = 1; //Contains only public_key_hash, not to be confused with TezosContractID
optional uint64 period = 2;
optional bytes proposal = 3;
optional TezosBallotType ballot = 4;
enum TezosBallotType {
Yay = 0;
Nay = 1;
Pass = 2;
}
}
}
/**

View File

@ -24,6 +24,13 @@ def write_uint16_le(w: bytearray, n: int) -> int:
return 2
def write_uint16_be(w: bytearray, n: int):
ensure(0 <= n <= 0xFFFF)
w.append((n >> 8) & 0xFF)
w.append(n & 0xFF)
return 2
def write_uint32_le(w: bytearray, n: int) -> int:
ensure(0 <= n <= 0xFFFFFFFF)
w.append(n & 0xFF)

View File

@ -3,6 +3,7 @@ from micropython import const
from trezor.crypto import base58
from apps.common import HARDENED
from apps.common.writers import write_uint8
TEZOS_AMOUNT_DIVISIBILITY = const(6)
TEZOS_ED25519_ADDRESS_PREFIX = "tz1"
@ -21,6 +22,8 @@ TEZOS_PREFIX_BYTES = {
"edsig": [9, 245, 205, 134, 18],
# operation hash
"o": [5, 116],
# protocol hash
"P": [2, 170],
}
@ -42,13 +45,29 @@ def validate_full_path(path: list) -> bool:
"""
Validates derivation path to equal 44'/1729'/a',
where `a` is an account index from 0 to 1 000 000.
Additional component added to allow ledger migration
44'/1729'/0'/b' where `b` is an account index from 0 to 1 000 000
"""
if len(path) != 3:
length = len(path)
if length < 3 or length > 4:
return False
if path[0] != 44 | HARDENED:
return False
if path[1] != 1729 | HARDENED:
return False
if path[2] < HARDENED or path[2] > 1000000 | HARDENED:
return False
if length == 3:
if path[2] < HARDENED or path[2] > 1000000 | HARDENED:
return False
if length == 4:
if path[2] != 0 | HARDENED:
return False
if path[3] < HARDENED or path[3] > 1000000 | HARDENED:
return False
return True
def write_bool(w: bytearray, boolean: bool):
if boolean:
write_uint8(w, 255)
else:
write_uint8(w, 0)

View File

@ -1,5 +1,10 @@
from trezor import ui
from trezor.messages import ButtonRequestType
from micropython import const
from trezor import ui, wire
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import CANCELLED, ConfirmDialog
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor.ui.text import Text
from trezor.utils import chunks, format_amount
@ -66,6 +71,45 @@ def split_address(address):
return chunks(address, 18)
def split_proposal(proposal):
return chunks(proposal, 17)
def format_tezos_amount(value):
formatted_value = format_amount(value, TEZOS_AMOUNT_DIVISIBILITY)
return formatted_value + " XTZ"
async def require_confirm_ballot(ctx, proposal, ballot):
text = Text("Submit ballot", ui.ICON_SEND, icon_color=ui.PURPLE)
text.bold("Ballot: {}".format(ballot))
text.bold("Proposal:")
text.mono(*split_proposal(proposal))
await require_confirm(ctx, text, ButtonRequestType.SignTx)
# use, when there are more then one proposals in one operation
async def require_confirm_proposals(ctx, proposals):
await ctx.call(ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck)
first_page = const(0)
pages = proposals
title = "Submit proposals" if len(proposals) > 1 else "Submit proposal"
paginator = paginate(show_proposal_page, len(pages), first_page, pages, title)
return await ctx.wait(paginator)
@ui.layout
async def show_proposal_page(page: int, page_count: int, pages: list, title: str):
text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE)
text.bold("Proposal {}: ".format(page + 1))
text.mono(*split_proposal(pages[page]))
content = Scrollpage(text, page, page_count)
if page + 1 >= page_count:
confirm = await ConfirmDialog(content)
if confirm == CANCELLED:
raise wire.ActionCancelled("Cancelled")
else:
content.render()
await animate_swipe()

View File

@ -1,13 +1,22 @@
from micropython import const
from trezor import wire
from trezor.crypto import hashlib
from trezor.crypto.curve import ed25519
from trezor.messages import TezosContractType
from trezor.messages import TezosBallotType, TezosContractType
from trezor.messages.TezosSignedTx import TezosSignedTx
from apps.common import paths
from apps.common.writers import write_bytes, write_uint8
from apps.common.writers import (
write_bytes,
write_uint8,
write_uint16_be,
write_uint32_be,
)
from apps.tezos import CURVE, helpers, layout
PROPOSAL_LENGTH = const(32)
async def sign_tx(ctx, msg, keychain):
await paths.validate_path(
@ -52,6 +61,15 @@ async def sign_tx(ctx, msg, keychain):
ctx, source, msg.delegation.fee
)
elif msg.proposal is not None:
proposed_protocols = [_get_protocol_hash(p) for p in msg.proposal.proposals]
await layout.require_confirm_proposals(ctx, proposed_protocols)
elif msg.ballot is not None:
proposed_protocol = _get_protocol_hash(msg.ballot.proposal)
submitted_ballot = _get_ballot(msg.ballot.ballot)
await layout.require_confirm_ballot(ctx, proposed_protocol, submitted_ballot)
else:
raise wire.DataError("Invalid operation")
@ -101,11 +119,24 @@ def _get_address_from_contract(address):
raise wire.DataError("Invalid contract type")
def _get_protocol_hash(proposal):
return helpers.base58_encode_check(proposal, prefix="P")
def _get_ballot(ballot):
if ballot == TezosBallotType.Yay:
return "yay"
elif ballot == TezosBallotType.Nay:
return "nay"
elif ballot == TezosBallotType.Pass:
return "pass"
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
# we need to reveal its public key
if msg.reveal is not None:
_encode_common(w, msg.reveal, "reveal")
write_bytes(w, msg.reveal.public_key)
@ -121,14 +152,18 @@ def _get_operation_bytes(w: bytearray, msg):
_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)
helpers.write_bool(w, msg.origination.spendable)
helpers.write_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)
elif msg.proposal is not None:
_encode_proposal(w, msg.proposal)
elif msg.ballot is not None:
_encode_ballot(w, msg.ballot)
def _encode_common(w: bytearray, operation, str_operation):
@ -146,19 +181,12 @@ def _encode_contract_id(w: bytearray, contract_id):
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)
helpers.write_bool(w, True)
write_bytes(w, data)
else:
_encode_bool(w, False)
helpers.write_bool(w, False)
def _encode_zarith(w: bytearray, num):
@ -171,3 +199,24 @@ def _encode_zarith(w: bytearray, num):
break
write_uint8(w, 128 | byte)
def _encode_proposal(w: bytearray, proposal):
proposal_tag = 5
write_uint8(w, proposal_tag)
write_bytes(w, proposal.source)
write_uint32_be(w, proposal.period)
write_uint32_be(w, len(proposal.proposals) * PROPOSAL_LENGTH)
for proposal_hash in proposal.proposals:
write_bytes(w, proposal_hash)
def _encode_ballot(w: bytearray, ballot):
ballot_tag = 6
write_uint8(w, ballot_tag)
write_bytes(w, ballot.source)
write_uint32_be(w, ballot.period)
write_bytes(w, ballot.proposal)
write_uint8(w, ballot.ballot)

View File

@ -0,0 +1,27 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class TezosBallotOp(p.MessageType):
def __init__(
self,
source: bytes = None,
period: int = None,
proposal: bytes = None,
ballot: int = None,
) -> None:
self.source = source
self.period = period
self.proposal = proposal
self.ballot = ballot
@classmethod
def get_fields(cls):
return {
1: ('source', p.BytesType, 0),
2: ('period', p.UVarintType, 0),
3: ('proposal', p.BytesType, 0),
4: ('ballot', p.UVarintType, 0),
}

View File

@ -0,0 +1,5 @@
# Automatically generated by pb2py
# fmt: off
Yay = 0
Nay = 1
Pass = 2

View File

@ -0,0 +1,30 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import List
except ImportError:
List = None # type: ignore
class TezosProposalOp(p.MessageType):
def __init__(
self,
source: bytes = None,
period: int = None,
proposals: List[bytes] = None,
) -> None:
self.source = source
self.period = period
self.proposals = proposals if proposals is not None else []
@classmethod
def get_fields(cls):
return {
1: ('source', p.BytesType, 0),
2: ('period', p.UVarintType, 0),
4: ('proposals', p.BytesType, p.FLAG_REPEATED),
}

View File

@ -2,8 +2,10 @@
# fmt: off
import protobuf as p
from .TezosBallotOp import TezosBallotOp
from .TezosDelegationOp import TezosDelegationOp
from .TezosOriginationOp import TezosOriginationOp
from .TezosProposalOp import TezosProposalOp
from .TezosRevealOp import TezosRevealOp
from .TezosTransactionOp import TezosTransactionOp
@ -25,6 +27,8 @@ class TezosSignTx(p.MessageType):
transaction: TezosTransactionOp = None,
origination: TezosOriginationOp = None,
delegation: TezosDelegationOp = None,
proposal: TezosProposalOp = None,
ballot: TezosBallotOp = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.branch = branch
@ -32,6 +36,8 @@ class TezosSignTx(p.MessageType):
self.transaction = transaction
self.origination = origination
self.delegation = delegation
self.proposal = proposal
self.ballot = ballot
@classmethod
def get_fields(cls):
@ -42,4 +48,6 @@ class TezosSignTx(p.MessageType):
4: ('transaction', TezosTransactionOp, 0),
5: ('origination', TezosOriginationOp, 0),
6: ('delegation', TezosDelegationOp, 0),
7: ('proposal', TezosProposalOp, 0),
8: ('ballot', TezosBallotOp, 0),
}

View File

@ -21,9 +21,9 @@ async def change_page(page, page_count):
else:
s = await swipe
if s == SWIPE_UP:
return page + 1 # scroll down
return min(page + 1, page_count - 1) # scroll down
elif s == SWIPE_DOWN:
return page - 1 # scroll up
return max(page - 1, 0) # scroll up
async def paginate(render_page, page_count, page=0, *args):

View File

@ -46,10 +46,11 @@ class TestTezosAddress(unittest.TestCase):
[44 | HARDENED],
[44 | HARDENED, 1729 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 0],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 0 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 0],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 0 | HARDENED, 0 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 1, 0],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 0, 0],
[44 | HARDENED, 1729 | HARDENED, 1 | HARDENED, 1 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 9999000 | HARDENED],
[44 | HARDENED, 60 | HARDENED, 0 | HARDENED, 0, 0],
[1 | HARDENED, 1 | HARDENED, 1 | HARDENED],
@ -58,6 +59,9 @@ class TestTezosAddress(unittest.TestCase):
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 3 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 9 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 0 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 3 | HARDENED],
[44 | HARDENED, 1729 | HARDENED, 0 | HARDENED, 9 | HARDENED],
]
for path in incorrect_paths:

View File

@ -4,9 +4,8 @@ from common import *
from trezor.messages import TezosContractType
from trezor.messages.TezosContractID import TezosContractID
from apps.tezos.helpers import base58_decode_check, base58_encode_check
from apps.tezos.helpers import base58_decode_check, base58_encode_check, write_bool
from apps.tezos.sign_tx import (
_encode_bool,
_encode_contract_id,
_encode_data_with_bool_prefix,
_encode_zarith,
@ -35,11 +34,11 @@ class TestTezosEncoding(unittest.TestCase):
def test_tezos_encode_bool(self):
w = bytearray()
_encode_bool(w, True)
write_bool(w, True)
self.assertEqual(bytes(w), bytes([255]))
w = bytearray()
_encode_bool(w, False)
write_bool(w, False)
self.assertEqual(bytes(w), bytes([0]))
def test_tezos_encode_contract_id(self):

View File

@ -14,6 +14,8 @@
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import time
import pytest
from trezorlib import messages, tezos
@ -23,6 +25,7 @@ from trezorlib.tools import parse_path
from .common import TrezorTest
TEZOS_PATH = parse_path("m/44'/1729'/0'")
TEZOS_PATH_10 = parse_path("m/44'/1729'/10'")
@pytest.mark.tezos
@ -194,3 +197,152 @@ class TestMsgTezosSignTx(TrezorTest):
assert (
resp.operation_hash == "oocgc3hyKsGHPsw6WFWJpWT8jBwQLtebQAXF27KNisThkzoj635"
)
def input_flow(self, num_pages):
yield
time.sleep(1)
for _ in range(num_pages - 1):
self.client.debug.swipe_down()
time.sleep(1)
self.client.debug.press_yes()
def test_tezos_sign_tx_proposal(self):
self.setup_mnemonic_allallall()
self.client.set_input_flow(self.input_flow(num_pages=1))
resp = tezos.sign_tx(
self.client,
TEZOS_PATH_10,
dict_to_proto(
messages.TezosSignTx,
{
"branch": "dee04042c0832d68a43699b2001c0a38065436eb05e578071a763e1972d0bc81",
"proposal": {
"source": "005f450441f41ee11eee78a31d1e1e55627c783bd6",
"period": 17,
"proposals": [
"dfa974df171c2dad9a9b8f25d99af41fd9702ce5d04521d2f9943c84d88aa572"
],
},
},
),
)
assert (
resp.signature
== "edsigtfY16R32k2WVMYfFr7ymnro4ib5zMckk28vsuViYNN77DJAvCJLRNArd9L531pUCxT4YdcvCvBym5dhcZ1rknEVm6yZ8bB"
)
assert (
resp.sig_op_contents.hex()
== "dee04042c0832d68a43699b2001c0a38065436eb05e578071a763e1972d0bc8105005f450441f41ee11eee78a31d1e1e55627c783bd60000001100000020dfa974df171c2dad9a9b8f25d99af41fd9702ce5d04521d2f9943c84d88aa5723b12621296a679b3a74ea790df5347995a76e20a09e76590baaacf4e09341965a04123f5cbbba8427f045b5f7d59157a3098e44839babe7c247d19b58bbb2405"
)
assert (
resp.operation_hash == "opLqntFUu984M7LnGsFvfGW6kWe9QjAz4AfPDqQvwJ1wPM4Si4c"
)
def test_tezos_sign_tx_multiple_proposals(self):
self.setup_mnemonic_allallall()
self.client.set_input_flow(self.input_flow(num_pages=2))
resp = tezos.sign_tx(
self.client,
TEZOS_PATH_10,
dict_to_proto(
messages.TezosSignTx,
{
"branch": "7e0be36a90c663c73c60da3889ffefff1383fb65cc29f0639f173d8f95a52df7",
"proposal": {
"source": "005f450441f41ee11eee78a31d1e1e55627c783bd6",
"period": 17,
"proposals": [
"2a6ff28ab4d0ccb18f7129aaaf9a4b8027d794f2562849665fdb6999db2a4e57",
"47cd60c09ab8437cc9fe19add494dce1b9844100f660f02ce77510a0c66d2762",
],
},
},
),
)
assert (
resp.signature
== "edsigu6GAjhiWAQ64ctWTGEDYAZ16tYzLgzWzqc4CUyixK4FGRE8YUBVzFaVJ2fUCexZjZLMLdiNZGcUdzeL1bQhZ2h5oLrh7pA"
)
assert (
resp.sig_op_contents.hex()
== "7e0be36a90c663c73c60da3889ffefff1383fb65cc29f0639f173d8f95a52df705005f450441f41ee11eee78a31d1e1e55627c783bd600000011000000402a6ff28ab4d0ccb18f7129aaaf9a4b8027d794f2562849665fdb6999db2a4e5747cd60c09ab8437cc9fe19add494dce1b9844100f660f02ce77510a0c66d2762f813361ac00ada7e3256f23973ae25b112229476a3cb3e506fe929ea1e9358299fed22178d1be689cddeedd1f303abfef859b664f159a528576a1c807079f005"
)
assert (
resp.operation_hash == "onobSyNgiitGXxSVFJN6949MhUomkkxvH4ZJ2owgWwNeDdntF9Y"
)
def test_tezos_sing_tx_ballot_yay(self):
self.setup_mnemonic_allallall()
resp = tezos.sign_tx(
self.client,
TEZOS_PATH_10,
dict_to_proto(
messages.TezosSignTx,
{
"branch": "3a8f60c4cd394cee5b50136c7fc8cb157e8aaa476a9e5c68709be6fc1cdb5395",
"ballot": {
"source": "0002298c03ed7d454a101eb7022bc95f7e5f41ac78",
"period": 2,
"proposal": "def7ed9c84af23ab37ebb60dd83cd103d1272ad6c63d4c05931567e65ed027e3",
"ballot": 0,
},
},
),
)
assert (
resp.signature
== "edsigtkxNm6YXwtV24DqeuimeZFTeFCn2jDYheSsXT4rHMcEjNvzsiSo55nVyVsQxtEe8M7U4PWJWT4rGYYGckQCgtkNJkd2roX"
)
def test_tezos_sing_tx_ballot_nay(self):
self.setup_mnemonic_allallall()
resp = tezos.sign_tx(
self.client,
TEZOS_PATH_10,
dict_to_proto(
messages.TezosSignTx,
{
"branch": "3a8f60c4cd394cee5b50136c7fc8cb157e8aaa476a9e5c68709be6fc1cdb5395",
"ballot": {
"source": "0002298c03ed7d454a101eb7022bc95f7e5f41ac78",
"period": 2,
"proposal": "def7ed9c84af23ab37ebb60dd83cd103d1272ad6c63d4c05931567e65ed027e3",
"ballot": 1,
},
},
),
)
assert (
resp.signature
== "edsigtqLaizfF6Cfc2JQL7TrsyniGhpZEojZAKMFW6AeudaUoU8KGXEHJH69Q4Lf27qFyUSTfbeHNnnCt69SGEPWkmpkgkgqMbL"
)
def test_tezos_sing_tx_ballot_pass(self):
self.setup_mnemonic_allallall()
resp = tezos.sign_tx(
self.client,
TEZOS_PATH_10,
dict_to_proto(
messages.TezosSignTx,
{
"branch": "3a8f60c4cd394cee5b50136c7fc8cb157e8aaa476a9e5c68709be6fc1cdb5395",
"ballot": {
"source": "0002298c03ed7d454a101eb7022bc95f7e5f41ac78",
"period": 2,
"proposal": "def7ed9c84af23ab37ebb60dd83cd103d1272ad6c63d4c05931567e65ed027e3",
"ballot": 2,
},
},
),
)
assert (
resp.signature
== "edsigu6YX7EegPwrpcEbdNQsNhrRiEagBNGJBmFamP4mixZZw1UynhahGQ8RNiZLSUVLERUZwygrsSVenBqXGt9VnknTxtzjKzv"
)