From 17a4ccd268707b2504a46a0ba3e03c1e319201d0 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 24 May 2018 15:33:03 +0200 Subject: [PATCH] stellar: transaction operations --- src/apps/stellar/consts.py | 40 +++++++ src/apps/stellar/operations.py | 192 +++++++++++++++++++++++++++++++++ src/apps/stellar/sign_tx.py | 32 +++--- src/apps/stellar/writers.py | 9 ++ 4 files changed, 254 insertions(+), 19 deletions(-) create mode 100644 src/apps/stellar/consts.py create mode 100644 src/apps/stellar/operations.py diff --git a/src/apps/stellar/consts.py b/src/apps/stellar/consts.py new file mode 100644 index 0000000000..87c9bd606d --- /dev/null +++ b/src/apps/stellar/consts.py @@ -0,0 +1,40 @@ +from trezor.messages import wire_types +from micropython import const + +# source: https://github.com/stellar/go/blob/master/xdr/Stellar-transaction.x +op_codes = { + 'StellarAccountMergeOp': const(8), + 'StellarAllowTrustOp': const(7), + 'StellarBumpSequenceOp': const(11), + 'StellarChangeTrustOp': const(6), + 'StellarCreateAccountOp': const(0), + 'StellarCreatePassiveOfferOp': const(4), + 'StellarManageDataOp': const(10), + 'StellarManageOfferOp': const(3), + 'StellarPathPaymentOp': const(2), + 'StellarPaymentOp': const(1), + 'StellarSetOptionsOp': const(5), +} + +op_wire_types = [ + wire_types.StellarAccountMergeOp, + wire_types.StellarAllowTrustOp, + wire_types.StellarBumpSequenceOp, + wire_types.StellarChangeTrustOp, + wire_types.StellarCreateAccountOp, + wire_types.StellarCreatePassiveOfferOp, + wire_types.StellarManageDataOp, + wire_types.StellarManageOfferOp, + wire_types.StellarPathPaymentOp, + wire_types.StellarPaymentOp, + wire_types.StellarSetOptionsOp, +] + +ASSET_TYPE_CREDIT_ALPHANUM4 = const(1) +ASSET_TYPE_CREDIT_ALPHANUM12 = const(2) + + +def get_op_code(msg) -> int: + if msg.__qualname__ not in op_codes: + raise ValueError('Stellar: op code unknown') + return op_codes[msg.__qualname__] diff --git a/src/apps/stellar/operations.py b/src/apps/stellar/operations.py new file mode 100644 index 0000000000..cc2538b143 --- /dev/null +++ b/src/apps/stellar/operations.py @@ -0,0 +1,192 @@ +from trezor.messages.StellarAccountMergeOp import StellarAccountMergeOp +from trezor.messages.StellarAssetType import StellarAssetType +from trezor.messages.StellarAllowTrustOp import StellarAllowTrustOp +from trezor.messages.StellarBumpSequenceOp import StellarBumpSequenceOp +from trezor.messages.StellarChangeTrustOp import StellarChangeTrustOp +from trezor.messages.StellarCreateAccountOp import StellarCreateAccountOp +from trezor.messages.StellarCreatePassiveOfferOp import StellarCreatePassiveOfferOp +from trezor.messages.StellarManageDataOp import StellarManageDataOp +from trezor.messages.StellarManageOfferOp import StellarManageOfferOp +from trezor.messages.StellarPathPaymentOp import StellarPathPaymentOp +from trezor.messages.StellarPaymentOp import StellarPaymentOp +from trezor.messages.StellarSetOptionsOp import StellarSetOptionsOp +from apps.stellar.consts import * +from apps.stellar.writers import * + +# todo layout + + +def serialize_op(w, op): + _serialize_account(w, op.source_account) + write_uint32(w, get_op_code(op)) + if isinstance(op, StellarAccountMergeOp): + serialize_account_merge_op(w, op) + elif isinstance(op, StellarAllowTrustOp): + serialize_allow_trust_op(w, op) + elif isinstance(op, StellarBumpSequenceOp): + serialize_bump_sequence_op(w, op) + elif isinstance(op, StellarChangeTrustOp): + serialize_change_trust_op(w, op) + elif isinstance(op, StellarCreateAccountOp): + serialize_create_account_op(w, op) + elif isinstance(op, StellarCreatePassiveOfferOp): + serialize_create_passive_offer_op(w, op) + elif isinstance(op, StellarManageDataOp): + serialize_manage_data_op(w, op) + elif isinstance(op, StellarManageOfferOp): + serialize_manage_offer_op(w, op) + elif isinstance(op, StellarPathPaymentOp): + serialize_path_payment_op(w, op) + elif isinstance(op, StellarPaymentOp): + serialize_payment_op(w, op) + elif isinstance(op, StellarSetOptionsOp): + serialize_set_options_op(w, op) + else: + raise ValueError('Stellar: unknown operation') + + +def serialize_account_merge_op(w, msg: StellarAccountMergeOp): + write_pubkey(w, msg.destination_account) + + +def serialize_allow_trust_op(w, msg: StellarAllowTrustOp): + # trustor account (the account being allowed to access the asset) + write_pubkey(w, msg.trusted_account) + write_uint32(w, msg.asset_type) + _serialize_asset_code(w, msg.asset_type, msg.asset_code) + + write_bool(w, msg.is_authorized) + + +def serialize_bump_sequence_op(w, msg: StellarBumpSequenceOp): + write_uint64(w, msg.bump_to) + + +def serialize_change_trust_op(w, msg: StellarChangeTrustOp): + _serialize_asset(w, msg.asset) + write_uint64(w, msg.limit) + + +def serialize_create_account_op(w, msg: StellarCreateAccountOp): + write_pubkey(w, msg.new_account) + write_uint64(w, msg.starting_balance) + + +def serialize_create_passive_offer_op(w, msg: StellarCreatePassiveOfferOp): + _serialize_asset(w, msg.selling_asset) + _serialize_asset(w, msg.buying_asset) + write_uint64(w, msg.amount) + write_uint32(w, msg.price_n) + write_uint32(w, msg.price_d) + + +def serialize_manage_data_op(w, msg: StellarManageDataOp): + if len(msg.key) > 64: + raise ValueError('Stellar: max length of a key is 64 bytes') + write_string(w, msg.key) + write_bool(w, bool(msg.value)) + if msg.value: + write_uint32(w, len(msg.value)) + write_bytes(w, msg.value) + + +def serialize_manage_offer_op(w, msg: StellarManageOfferOp): + _serialize_asset(w, msg.selling_asset) + _serialize_asset(w, msg.buying_asset) + write_uint64(w, msg.amount) # amount to sell + write_uint32(w, msg.price_n) # numerator + write_uint32(w, msg.price_d) # denominator + write_uint64(w, msg.offer_id) + + +def serialize_path_payment_op(w, msg: StellarPathPaymentOp): + _serialize_asset(w, msg.send_asset) + write_uint64(w, msg.send_max) + write_pubkey(w, msg.destination_account) + + _serialize_asset(w, msg.destination_asset) + write_uint64(w, msg.destination_amount) + write_uint32(w, len(msg.paths)) + for p in msg.paths: + _serialize_asset(w, p) + + +def serialize_payment_op(w, msg: StellarPaymentOp): + write_pubkey(w, msg.destination_account) + _serialize_asset(w, msg.asset) + write_uint64(w, msg.amount) + + +def serialize_set_options_op(w, msg: StellarSetOptionsOp): + # inflation destination + write_bool(w, bool(msg.inflation_destination_account)) + if msg.inflation_destination_account: + write_pubkey(w, msg.inflation_destination_account) + + # clear flags + write_bool(w, bool(msg.clear_flags)) + if msg.clear_flags: + write_uint32(w, msg.clear_flags) + + # set flags + write_bool(w, bool(msg.set_flags)) + if msg.set_flags: + write_uint32(w, msg.set_flags) + + # account thresholds + write_bool(w, bool(msg.master_weight)) + if msg.master_weight: + write_uint32(w, msg.master_weight) + + write_bool(w, bool(msg.low_threshold)) + if msg.low_threshold: + write_uint32(w, msg.low_threshold) + + write_bool(w, bool(msg.medium_threshold)) + if msg.medium_threshold: + write_uint32(w, msg.medium_threshold) + + write_bool(w, bool(msg.high_threshold)) + if msg.high_threshold: + write_uint32(w, msg.high_threshold) + + # home domain + write_bool(w, bool(msg.home_domain)) + if msg.home_domain: + if len(msg.home_domain) > 32: + raise ValueError('Stellar: max length of a home domain is 32 bytes') + write_string(w, msg.home_domain) + + # signer + write_bool(w, bool(msg.signer_type)) + if msg.signer_type: + # signer type + write_uint32(w, msg.signer_type) + write_bytes(w, msg.signer_key) + write_uint32(w, msg.signer_weight) + + +def _serialize_account(w, source_account: bytes): + if source_account is None: + write_bool(w, False) + return + + write_pubkey(w, source_account) + + +def _serialize_asset_code(w, asset_type: int, asset_code: str): + code = bytearray(asset_code) + if asset_type == ASSET_TYPE_CREDIT_ALPHANUM4: + # pad with zeros to 4 chars + write_bytes(w, code + bytearray([0] * (4 - len(code)))) + elif asset_type == ASSET_TYPE_CREDIT_ALPHANUM12: + # pad with zeros to 12 chars + write_bytes(w, code + bytearray([0] * (12 - len(code)))) + else: + raise ValueError('Stellar: invalid asset type') + + +def _serialize_asset(w, asset: StellarAssetType): + write_uint32(w, asset.type) + _serialize_asset_code(w, asset.type, asset.code) + write_pubkey(w, asset.issuer) diff --git a/src/apps/stellar/sign_tx.py b/src/apps/stellar/sign_tx.py index e8e03c38bd..19c90d39b1 100644 --- a/src/apps/stellar/sign_tx.py +++ b/src/apps/stellar/sign_tx.py @@ -1,10 +1,10 @@ -from trezor.messages.StellarAccountMergeOp import StellarAccountMergeOp +from apps.common import seed +from apps.stellar.writers import * +from apps.stellar.operations import serialize_op +from apps.stellar.consts import op_wire_types from trezor.messages.StellarSignTx import StellarSignTx from trezor.messages.StellarTxOpRequest import StellarTxOpRequest from trezor.messages.StellarSignedTx import StellarSignedTx -from trezor.messages import wire_types -from .writers import * -from ..common import seed from trezor.crypto.curve import ed25519 from trezor.crypto.hashlib import sha256 @@ -18,15 +18,18 @@ async def sign_tx_loop(ctx, msg: StellarSignTx): while True: req = signer.send(res) if isinstance(req, StellarTxOpRequest): - res = await ctx.call(req, wire_types.StellarAccountMergeOp) + res = await ctx.call(req, *op_wire_types) elif isinstance(req, StellarSignedTx): break else: - raise TypeError('Invalid Stellar signing instruction') + raise TypeError('Stellar: Invalid signing instruction') return req async def sign_tx(ctx, msg): + if msg.num_operations == 0: + raise ValueError('Stellar: At least one operation is required') + network_passphrase_hash = sha256(msg.network_passphrase).digest() # Stellar transactions consist of sha256 of: @@ -57,7 +60,9 @@ async def sign_tx(ctx, msg): write_uint32(w, msg.memo_type) if msg.memo_type == 1: # nothing is needed for memo_type = 0 # Text: 4 bytes (size) + up to 28 bytes - write_bytes(w, bytearray(msg.memo_text)) # todo trim to 28 bytes? yes max 28 bytes! + if len(msg.memo_text) > 28: + raise ValueError('Stellar: max length of a memo text is 28 bytes') + write_string(w, msg.memo_text) elif msg.memo_type == 2: # ID: 64 bit unsigned integer write_uint64(w, msg.memo_id) @@ -68,10 +73,7 @@ async def sign_tx(ctx, msg): write_uint32(w, msg.num_operations) for i in range(msg.num_operations): op = yield StellarTxOpRequest() - # todo ask - # todo serialize OP - if isinstance(op, StellarAccountMergeOp): - serialize_account_merge_op(w, op) + serialize_op(w, op) # # Determine what type of network this transaction is for - todo used for layout # if msg.network_passphrase == "Public Global Stellar Network ; September 2015": @@ -98,14 +100,6 @@ async def sign_tx(ctx, msg): yield resp -def serialize_account_merge_op(w, msg: StellarAccountMergeOp): - if not msg.source_account: - write_bool(w, False) # todo move this to stellar_confirmSourceAccount - #else: todo ask and hash the address - write_uint32(w, 8) # merge op todo - write_pubkey(w, msg.destination_account) - - def node_derive(root, address_n: list): node = root.clone() node.derive_path(address_n) diff --git a/src/apps/stellar/writers.py b/src/apps/stellar/writers.py index db633b3f96..606f742b07 100644 --- a/src/apps/stellar/writers.py +++ b/src/apps/stellar/writers.py @@ -9,6 +9,15 @@ def write_uint64(w, n: int): write_bytes(w, ustruct.pack('>Q', n)) +def write_string(w, s: str): + write_uint32(w, len(s)) + write_bytes(w, bytearray(s)) + # if len isn't a multiple of 4, add padding bytes + reminder = len(s) % 4 + if reminder: + write_bytes(w, bytearray([0] * (4 - reminder))) + + def write_bytes(w, buf: bytearray): w.extend(buf)