You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/apps/tezos/sign_tx.py

418 lines
15 KiB

from typing import TYPE_CHECKING
from trezor import wire
from trezor.crypto import hashlib
from trezor.crypto.curve import ed25519
from trezor.enums import TezosBallotType, TezosContractType
from trezor.messages import TezosSignedTx
from apps.common import paths
from apps.common.keychain import with_slip44_keychain
from apps.common.writers import (
write_bytes_fixed,
write_bytes_unchecked,
write_uint8,
write_uint32_be,
)
from . import CURVE, PATTERNS, SLIP44_ID, helpers, layout
if TYPE_CHECKING:
from apps.common.keychain import Keychain
from trezor.wire import Context
from trezor.messages import (
TezosSignTx,
TezosContractID,
TezosManagerTransfer,
TezosBallotOp,
TezosProposalOp,
TezosRevealOp,
TezosDelegationOp,
TezosTransactionOp,
TezosOriginationOp,
)
from trezor.utils import Writer
@with_slip44_keychain(*PATTERNS, slip44_id=SLIP44_ID, curve=CURVE)
async def sign_tx(ctx: Context, msg: TezosSignTx, keychain: Keychain) -> TezosSignedTx:
await paths.validate_path(ctx, keychain, msg.address_n)
node = keychain.derive(msg.address_n)
if msg.transaction is not None:
# if the transaction operation is used to execute code on a smart contract
if msg.transaction.parameters_manager is not None:
parameters_manager = msg.transaction.parameters_manager
# operation to delegate from a smart contract with manager.tz
if parameters_manager.set_delegate is not None:
delegate = _get_address_by_tag(parameters_manager.set_delegate)
await layout.require_confirm_delegation_baker(ctx, delegate)
await layout.require_confirm_set_delegate(ctx, msg.transaction.fee)
# operation to remove delegate from the smart contract with manager.tz
elif parameters_manager.cancel_delegate is not None:
address = _get_address_from_contract(msg.transaction.destination)
await layout.require_confirm_delegation_manager_withdraw(ctx, address)
await layout.require_confirm_manager_remove_delegate(
ctx, msg.transaction.fee
)
# operation to transfer tokens from a smart contract to an implicit account or a smart contract
elif parameters_manager.transfer is not None:
to = _get_address_from_contract(parameters_manager.transfer.destination)
await layout.require_confirm_tx(
ctx, to, parameters_manager.transfer.amount
)
await layout.require_confirm_fee(
ctx, parameters_manager.transfer.amount, msg.transaction.fee
)
else:
# transactions from an implicit account
to = _get_address_from_contract(msg.transaction.destination)
await layout.require_confirm_tx(ctx, to, msg.transaction.amount)
await layout.require_confirm_fee(
ctx, msg.transaction.amount, msg.transaction.fee
)
elif msg.origination is not None:
source = _get_address_by_tag(msg.origination.source)
await layout.require_confirm_origination(ctx, source)
# if we are immediately delegating contract
if msg.origination.delegate is not None:
delegate = _get_address_by_tag(msg.origination.delegate)
await layout.require_confirm_delegation_baker(ctx, delegate)
await layout.require_confirm_origination_fee(
ctx, msg.origination.balance, msg.origination.fee
)
elif msg.delegation is not None:
source = _get_address_by_tag(msg.delegation.source)
delegate_address: str | None = None
if msg.delegation.delegate is not None:
delegate_address = _get_address_by_tag(msg.delegation.delegate)
if delegate_address is not None and source != delegate_address:
await layout.require_confirm_delegation_baker(ctx, delegate_address)
await layout.require_confirm_set_delegate(ctx, msg.delegation.fee)
# if account registers itself as a delegate
else:
await layout.require_confirm_register_delegate(
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")
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 = helpers.base58_encode_check(sig_op_contents_hash, prefix="o")
sig_prefixed = helpers.base58_encode_check(
signature, prefix=helpers.TEZOS_SIGNATURE_PREFIX
)
return TezosSignedTx(
signature=sig_prefixed, sig_op_contents=sig_op_contents, operation_hash=ophash
)
def _get_address_by_tag(address_hash: bytes) -> str:
prefixes = ["tz1", "tz2", "tz3"]
tag = int(address_hash[0])
if 0 <= tag < len(prefixes):
return helpers.base58_encode_check(address_hash[1:], prefix=prefixes[tag])
raise wire.DataError("Invalid tag in address hash")
def _get_address_from_contract(address: TezosContractID) -> str:
if address.tag == TezosContractType.Implicit:
return _get_address_by_tag(address.hash)
elif address.tag == TezosContractType.Originated:
return helpers.base58_encode_check(
address.hash[:-1], prefix=helpers.TEZOS_ORIGINATED_ADDRESS_PREFIX
)
raise wire.DataError("Invalid contract type")
def _get_protocol_hash(proposal: bytes) -> str:
return helpers.base58_encode_check(proposal, prefix="P")
def _get_ballot(ballot: TezosBallotType) -> str:
if ballot == TezosBallotType.Yay:
return "yay"
elif ballot == TezosBallotType.Nay:
return "nay"
elif ballot == TezosBallotType.Pass:
return "pass"
raise RuntimeError # unrecognized enum value
def _get_operation_bytes(w: Writer, msg: TezosSignTx) -> None:
write_bytes_fixed(w, msg.branch, helpers.BRANCH_HASH_SIZE)
# when the account sends first operation in lifetime,
# we need to reveal its public key
if msg.reveal is not None:
_encode_common(w, msg.reveal, "reveal")
tag = int(msg.reveal.public_key[0])
try:
public_key_size = helpers.PUBLIC_KEY_TAG_TO_SIZE[tag]
except KeyError:
raise wire.DataError("Invalid tag in public key")
write_bytes_fixed(w, msg.reveal.public_key, 1 + public_key_size)
# 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)
# support delegation and transfer from the old scriptless contracts (now with manager.tz script)
if msg.transaction.parameters_manager is not None:
parameters_manager = msg.transaction.parameters_manager
if parameters_manager.set_delegate is not None:
_encode_manager_delegation(w, parameters_manager.set_delegate)
elif parameters_manager.cancel_delegate is not None:
_encode_manager_delegation_remove(w)
elif parameters_manager.transfer is not None:
assert parameters_manager.transfer.destination is not None
if (
parameters_manager.transfer.destination.tag
== TezosContractType.Implicit
):
_encode_manager_to_implicit_transfer(w, parameters_manager.transfer)
else:
_encode_manager_to_manager_transfer(w, parameters_manager.transfer)
else:
if msg.transaction.parameters:
helpers.write_bool(w, True)
helpers.check_tx_params_size(msg.transaction.parameters)
write_bytes_unchecked(w, msg.transaction.parameters)
else:
helpers.write_bool(w, False)
# origination operation
elif msg.origination is not None:
_encode_common(w, msg.origination, "origination")
_encode_zarith(w, msg.origination.balance)
_encode_data_with_bool_prefix(
w, msg.origination.delegate, helpers.TAGGED_PUBKEY_HASH_SIZE
)
helpers.check_script_size(msg.origination.script)
write_bytes_unchecked(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, helpers.TAGGED_PUBKEY_HASH_SIZE
)
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: Writer,
operation: TezosDelegationOp
| TezosOriginationOp
| TezosTransactionOp
| TezosRevealOp,
str_operation: str,
) -> None:
operation_tags = {
"reveal": helpers.OP_TAG_REVEAL,
"transaction": helpers.OP_TAG_TRANSACTION,
"origination": helpers.OP_TAG_ORIGINATION,
"delegation": helpers.OP_TAG_DELEGATION,
}
write_uint8(w, operation_tags[str_operation])
write_bytes_fixed(w, operation.source, helpers.TAGGED_PUBKEY_HASH_SIZE)
_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: Writer, contract_id: TezosContractID) -> None:
write_uint8(w, contract_id.tag)
write_bytes_fixed(w, contract_id.hash, helpers.CONTRACT_ID_SIZE - 1)
def _encode_data_with_bool_prefix(
w: Writer, data: bytes | None, expected_length: int
) -> None:
if data:
helpers.write_bool(w, True)
write_bytes_fixed(w, data, expected_length)
else:
helpers.write_bool(w, False)
def _encode_zarith(w: Writer, num: int) -> None:
while True:
byte = num & 127
num = num >> 7
if num == 0:
write_uint8(w, byte)
break
write_uint8(w, 128 | byte)
def _encode_proposal(w: Writer, proposal: TezosProposalOp) -> None:
write_uint8(w, helpers.OP_TAG_PROPOSALS)
write_bytes_fixed(w, proposal.source, helpers.TAGGED_PUBKEY_HASH_SIZE)
write_uint32_be(w, proposal.period)
write_uint32_be(w, len(proposal.proposals) * helpers.PROPOSAL_HASH_SIZE)
for proposal_hash in proposal.proposals:
write_bytes_fixed(w, proposal_hash, helpers.PROPOSAL_HASH_SIZE)
def _encode_ballot(w: Writer, ballot: TezosBallotOp) -> None:
write_uint8(w, helpers.OP_TAG_BALLOT)
write_bytes_fixed(w, ballot.source, helpers.TAGGED_PUBKEY_HASH_SIZE)
write_uint32_be(w, ballot.period)
write_bytes_fixed(w, ballot.proposal, helpers.PROPOSAL_HASH_SIZE)
write_uint8(w, ballot.ballot)
def _encode_natural(w: Writer, num: int) -> None:
# encode a natural integer with its signed bit on position 7
# as we do not expect negative numbers in a transfer operation the bit is never set
natural_tag = 0
write_uint8(w, natural_tag)
byte = num & 63
modified = num >> 6
if modified == 0:
write_uint8(w, byte)
else:
write_uint8(w, 128 | byte)
_encode_zarith(w, modified)
def _encode_manager_common(
w: Writer, sequence_length: int, operation: str, to_contract: bool = False
) -> None:
# 5 = tag and sequence_length (1 byte + 4 bytes)
argument_length = sequence_length + 5
helpers.write_bool(w, True)
write_uint8(w, helpers.DO_ENTRYPOINT_TAG)
write_uint32_be(w, argument_length)
write_uint8(w, helpers.MICHELSON_SEQUENCE_TAG)
write_uint32_be(w, sequence_length)
helpers.write_instruction(w, "DROP")
helpers.write_instruction(w, "NIL")
helpers.write_instruction(w, "operation")
helpers.write_instruction(w, operation)
if to_contract:
helpers.write_instruction(w, "address")
else:
helpers.write_instruction(w, "key_hash")
if operation == "PUSH":
write_uint8(w, 10) # byte sequence
if to_contract:
write_uint32_be(w, helpers.CONTRACT_ID_SIZE)
else:
write_uint32_be(w, helpers.TAGGED_PUBKEY_HASH_SIZE)
def _encode_manager_to_implicit_transfer(
w: Writer, manager_transfer: TezosManagerTransfer
) -> None:
MICHELSON_LENGTH = 48
value_natural = bytearray()
_encode_natural(value_natural, manager_transfer.amount)
sequence_length = MICHELSON_LENGTH + len(value_natural)
_encode_manager_common(w, sequence_length, "PUSH")
write_bytes_fixed(
w, manager_transfer.destination.hash, helpers.TAGGED_PUBKEY_HASH_SIZE
)
helpers.write_instruction(w, "IMPLICIT_ACCOUNT")
helpers.write_instruction(w, "PUSH")
helpers.write_instruction(w, "mutez")
_encode_natural(w, manager_transfer.amount)
helpers.write_instruction(w, "UNIT")
helpers.write_instruction(w, "TRANSFER_TOKENS")
helpers.write_instruction(w, "CONS")
# smart_contract_delegation
def _encode_manager_delegation(w: Writer, delegate: bytes) -> None:
MICHELSON_LENGTH = 42 # length is fixed this time(no variable length fields)
_encode_manager_common(w, MICHELSON_LENGTH, "PUSH")
write_bytes_fixed(w, delegate, helpers.TAGGED_PUBKEY_HASH_SIZE)
helpers.write_instruction(w, "SOME")
helpers.write_instruction(w, "SET_DELEGATE")
helpers.write_instruction(w, "CONS")
def _encode_manager_delegation_remove(w: Writer) -> None:
MICHELSON_LENGTH = 14 # length is fixed this time(no variable length fields)
_encode_manager_common(w, MICHELSON_LENGTH, "NONE")
helpers.write_instruction(w, "SET_DELEGATE")
helpers.write_instruction(w, "CONS")
def _encode_manager_to_manager_transfer(
w: Writer, manager_transfer: TezosManagerTransfer
) -> None:
MICHELSON_LENGTH = 77
value_natural = bytearray()
_encode_natural(value_natural, manager_transfer.amount)
sequence_length = MICHELSON_LENGTH + len(value_natural)
_encode_manager_common(w, sequence_length, "PUSH", to_contract=True)
_encode_contract_id(w, manager_transfer.destination)
helpers.write_instruction(w, "CONTRACT")
helpers.write_instruction(w, "unit")
helpers.write_instruction(w, "ASSERT_SOME")
helpers.write_instruction(w, "PUSH")
helpers.write_instruction(w, "mutez")
_encode_natural(w, manager_transfer.amount)
helpers.write_instruction(w, "UNIT")
helpers.write_instruction(w, "TRANSFER_TOKENS")
helpers.write_instruction(w, "CONS")