diff --git a/common/protob/messages-tezos.proto b/common/protob/messages-tezos.proto index 1bc37b8a0..d6fbfea65 100644 --- a/common/protob/messages-tezos.proto +++ b/common/protob/messages-tezos.proto @@ -126,19 +126,24 @@ message TezosSignTx { * Structure representing information for proposal */ message TezosProposalOp { - optional bytes source = 1; + optional bytes source = 1; //Contains only public_key_hash, not to be confused with TezosContractID optional uint64 period = 2; - optional uint64 bytes_in_next_field = 3; - optional bytes proposals = 4; + repeated bytes proposals = 4; } /** * Structure representing information for ballot */ message TezosBallotOp { - optional bytes source = 1; + optional bytes source = 1; //Contains only public_key_hash, not to be confused with TezosContractID optional uint64 period = 2; optional bytes proposal = 3; - optional bytes ballot = 4; + optional TezosBallotType ballot = 4; + + enum TezosBallotType { + Yay = 0; + Nay = 1; + Pass = 2; + } } } diff --git a/core/src/apps/common/writers.py b/core/src/apps/common/writers.py index a4ea21a58..feeb32590 100644 --- a/core/src/apps/common/writers.py +++ b/core/src/apps/common/writers.py @@ -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) diff --git a/core/src/apps/tezos/helpers.py b/core/src/apps/tezos/helpers.py index 102b0b6e5..e93b49388 100644 --- a/core/src/apps/tezos/helpers.py +++ b/core/src/apps/tezos/helpers.py @@ -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,7 +22,7 @@ TEZOS_PREFIX_BYTES = { "edsig": [9, 245, 205, 134, 18], # operation hash "o": [5, 116], - # protocola hash + # protocol hash "P": [2, 170], } @@ -45,7 +46,8 @@ 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. """ - if len(path) != 3: + length = len(path) + if length < 3 or length > 4: return False if path[0] != 44 | HARDENED: return False @@ -53,4 +55,13 @@ def validate_full_path(path: list) -> bool: return False if path[2] < HARDENED or path[2] > 1000000 | HARDENED: return False + if length > 3 and (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) diff --git a/core/src/apps/tezos/layout.py b/core/src/apps/tezos/layout.py index 14d474e31..315875fea 100644 --- a/core/src/apps/tezos/layout.py +++ b/core/src/apps/tezos/layout.py @@ -79,18 +79,11 @@ def format_tezos_amount(value): return formatted_value + " XTZ" -async def require_confirm_proposal(ctx, proposals): - text = Text("Submit proposal", ui.ICON_SEND, icon_color=ui.PURPLE) - text.bold("Proposal:") - text.mono(*split_proposal(proposals[0])) - await require_confirm(ctx, text, ButtonRequestType.SignTx) - - 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[0])) + text.mono(*split_proposal(proposal)) await require_confirm(ctx, text, ButtonRequestType.SignTx) @@ -98,15 +91,15 @@ async def require_confirm_ballot(ctx, proposal, ballot): async def show_proposals(ctx, proposals): 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) + 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): - - text = Text("Submit proposals", ui.ICON_SEND, icon_color=ui.PURPLE) +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) diff --git a/core/src/apps/tezos/sign_tx.py b/core/src/apps/tezos/sign_tx.py index 987ef553f..98c4ec4de 100644 --- a/core/src/apps/tezos/sign_tx.py +++ b/core/src/apps/tezos/sign_tx.py @@ -3,18 +3,16 @@ 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 TezosContractType, TezosBallotType from trezor.messages.TezosSignedTx import TezosSignedTx from apps.common import paths from apps.tezos import CURVE, helpers, layout -from apps.tezos.writers import ( - write_bool, +from apps.common.writers import ( write_bytes, write_uint8, - write_uint16, - write_uint32, - write_uint64, + write_uint16_be, + write_uint32_be, ) PROPOSAL_LENGTH = const(32) @@ -65,12 +63,7 @@ async def sign_tx(ctx, msg, keychain): elif msg.proposal is not None: proposed_protocols = _get_protocol_hash_from_msg(msg.proposal.proposals) - - # byte count larger than PROPOSAL_LENGTH indicates more than 1 proposal, use paginated screen - if msg.proposal.bytes_in_next_field > PROPOSAL_LENGTH: - await layout.show_proposals(ctx, proposed_protocols) - else: - await layout.require_confirm_proposal(ctx, proposed_protocols) + await layout.show_proposals(ctx, proposed_protocols) elif msg.ballot is not None: proposed_protocol = _get_protocol_hash_from_msg(msg.ballot.proposal) @@ -127,25 +120,20 @@ def _get_address_from_contract(address): def _get_protocol_hash_from_msg(proposals): - # split the proposals - proposal_list = list( - [ - proposals[i : i + PROPOSAL_LENGTH] - for i in range(0, len(proposals), PROPOSAL_LENGTH) - ] - ) + print(proposals) + if type(proposals) is not list: + return helpers.base58_encode_check(proposals, prefix="P") return [ - helpers.base58_encode_check(proposal, prefix="P") for proposal in proposal_list + helpers.base58_encode_check(proposal, prefix="P") for proposal in proposals ] -def _get_ballot(encoded_ballot): - encoded_ballot = int(encoded_ballot[0]) - if encoded_ballot == 0: +def _get_ballot(ballot): + if ballot == TezosBallotType.Yay: return "yay" - elif encoded_ballot == 1: + elif ballot == TezosBallotType.Nay: return "nay" - elif encoded_ballot == 2: + elif ballot == TezosBallotType.Pass: return "pass" @@ -169,8 +157,8 @@ 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 @@ -198,19 +186,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): @@ -230,9 +211,10 @@ def _encode_proposal(w: bytearray, proposal): write_uint8(w, proposal_tag) write_bytes(w, proposal.source) - write_uint32(w, proposal.period) - write_uint32(w, proposal.bytes_in_next_field) - write_bytes(w, proposal.proposals) + write_uint32_be(w, proposal.period) + write_uint32_be(w, _get_proposals_bytecount(proposal.proposals)) + for proposal_hash in proposal.proposals: + write_bytes(w, proposal_hash) def _encode_ballot(w: bytearray, ballot): @@ -240,6 +222,10 @@ def _encode_ballot(w: bytearray, ballot): write_uint8(w, ballot_tag) write_bytes(w, ballot.source) - write_uint32(w, ballot.period) + write_uint32_be(w, ballot.period) write_bytes(w, ballot.proposal) - write_bytes(w, ballot.ballot) + write_uint8(w, ballot.ballot) + + +def _get_proposals_bytecount(proposals): + return len(proposals) * PROPOSAL_LENGTH diff --git a/core/src/trezor/messages/TezosBallotOp.py b/core/src/trezor/messages/TezosBallotOp.py index 260bf2a4b..cf6b969cd 100644 --- a/core/src/trezor/messages/TezosBallotOp.py +++ b/core/src/trezor/messages/TezosBallotOp.py @@ -10,7 +10,7 @@ class TezosBallotOp(p.MessageType): source: bytes = None, period: int = None, proposal: bytes = None, - ballot: bytes = None, + ballot: int = None, ) -> None: self.source = source self.period = period @@ -23,5 +23,5 @@ class TezosBallotOp(p.MessageType): 1: ('source', p.BytesType, 0), 2: ('period', p.UVarintType, 0), 3: ('proposal', p.BytesType, 0), - 4: ('ballot', p.BytesType, 0), + 4: ('ballot', p.UVarintType, 0), } diff --git a/core/src/trezor/messages/TezosBallotType.py b/core/src/trezor/messages/TezosBallotType.py new file mode 100644 index 000000000..1d955862f --- /dev/null +++ b/core/src/trezor/messages/TezosBallotType.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +Yay = 0 +Nay = 1 +Pass = 2 diff --git a/core/src/trezor/messages/TezosProposalOp.py b/core/src/trezor/messages/TezosProposalOp.py index 94f0dd532..d533ce6c9 100644 --- a/core/src/trezor/messages/TezosProposalOp.py +++ b/core/src/trezor/messages/TezosProposalOp.py @@ -2,6 +2,12 @@ # fmt: off import protobuf as p +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + class TezosProposalOp(p.MessageType): @@ -9,19 +15,16 @@ class TezosProposalOp(p.MessageType): self, source: bytes = None, period: int = None, - bytes_in_next_field: int = None, - proposals: bytes = None, + proposals: List[bytes] = None, ) -> None: self.source = source self.period = period - self.bytes_in_next_field = bytes_in_next_field - self.proposals = proposals + 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), - 3: ('bytes_in_next_field', p.UVarintType, 0), - 4: ('proposals', p.BytesType, 0), + 4: ('proposals', p.BytesType, p.FLAG_REPEATED), } diff --git a/python/trezorlib/tests/device_tests/test_msg_tezos_sign_tx.py b/python/trezorlib/tests/device_tests/test_msg_tezos_sign_tx.py index d0478b576..db5643c38 100644 --- a/python/trezorlib/tests/device_tests/test_msg_tezos_sign_tx.py +++ b/python/trezorlib/tests/device_tests/test_msg_tezos_sign_tx.py @@ -23,6 +23,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 +195,140 @@ class TestMsgTezosSignTx(TrezorTest): assert ( resp.operation_hash == "oocgc3hyKsGHPsw6WFWJpWT8jBwQLtebQAXF27KNisThkzoj635" ) + + def test_tezos_sign_tx_proposal(self): + self.setup_mnemonic_allallall() + + 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() + + 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" + ) +