|
| 1 | +""" |
| 2 | +Initializes a new transaction. |
| 3 | +""" |
| 4 | + |
| 5 | +import gc |
| 6 | + |
| 7 | +from apps.monero.controller import misc |
| 8 | +from apps.monero.layout import confirms |
| 9 | +from apps.monero.protocol.signing.state import State |
| 10 | +from apps.monero.xmr import common, crypto, monero |
| 11 | + |
| 12 | + |
| 13 | +async def init_transaction(ctx, address_n, network_type, tsx_data): |
| 14 | + from apps.monero.xmr.sub.addr import classify_subaddresses |
| 15 | + from apps.monero.protocol import hmac_encryption_keys |
| 16 | + |
| 17 | + state = State(ctx) |
| 18 | + state.creds = await misc.monero_get_creds(ctx, address_n, network_type) |
| 19 | + |
| 20 | + state.tx_priv = crypto.random_scalar() |
| 21 | + state.tx_pub = crypto.scalarmult_base(state.tx_priv) |
| 22 | + |
| 23 | + state._mem_trace(1) |
| 24 | + |
| 25 | + # Ask for confirmation |
| 26 | + await confirms.confirm_transaction(state.ctx, tsx_data, state.creds) |
| 27 | + gc.collect() |
| 28 | + state._mem_trace(3) |
| 29 | + |
| 30 | + # Basic transaction parameters |
| 31 | + state.input_count = tsx_data.num_inputs |
| 32 | + state.output_count = len(tsx_data.outputs) |
| 33 | + state.output_change = misc.dst_entry_to_stdobj(tsx_data.change_dts) |
| 34 | + state.mixin = tsx_data.mixin |
| 35 | + state.fee = tsx_data.fee |
| 36 | + state.account_idx = tsx_data.account |
| 37 | + state.multi_sig = tsx_data.is_multisig |
| 38 | + check_change(state, tsx_data.outputs) |
| 39 | + state.exp_tx_prefix_hash = tsx_data.exp_tx_prefix_hash |
| 40 | + |
| 41 | + # Rsig data |
| 42 | + state.rsig_type = tsx_data.rsig_data.rsig_type |
| 43 | + state.rsig_grp = tsx_data.rsig_data.grouping |
| 44 | + state.rsig_offload = state.rsig_type > 0 and state.output_count > 2 |
| 45 | + state.use_bulletproof = state.rsig_type > 0 |
| 46 | + state.use_simple_rct = state.input_count > 1 or state.rsig_type != 0 |
| 47 | + |
| 48 | + # Provided tx key, used mostly in multisig. |
| 49 | + if len(tsx_data.use_tx_keys) > 0: |
| 50 | + for ckey in tsx_data.use_tx_keys: |
| 51 | + crypto.check_sc(crypto.decodeint(ckey)) |
| 52 | + |
| 53 | + state.tx_priv = crypto.decodeint(tsx_data.use_tx_keys[0]) |
| 54 | + state.tx_pub = crypto.scalarmult_base(state.tx_priv) |
| 55 | + state.additional_tx_private_keys = [ |
| 56 | + crypto.decodeint(x) for x in tsx_data.use_tx_keys[1:] |
| 57 | + ] |
| 58 | + |
| 59 | + # Additional keys w.r.t. subaddress destinations |
| 60 | + class_res = classify_subaddresses(tsx_data.outputs, state.change_address()) |
| 61 | + num_stdaddresses, num_subaddresses, single_dest_subaddress = class_res |
| 62 | + |
| 63 | + # if this is a single-destination transfer to a subaddress, we set the tx pubkey to R=r*D |
| 64 | + if num_stdaddresses == 0 and num_subaddresses == 1: |
| 65 | + state.tx_pub = crypto.scalarmult( |
| 66 | + crypto.decodepoint(single_dest_subaddress.spend_public_key), state.tx_priv |
| 67 | + ) |
| 68 | + |
| 69 | + state.need_additional_txkeys = num_subaddresses > 0 and ( |
| 70 | + num_stdaddresses > 0 or num_subaddresses > 1 |
| 71 | + ) |
| 72 | + state._mem_trace(4, True) |
| 73 | + |
| 74 | + # Extra processing, payment id |
| 75 | + state.tx.version = 2 |
| 76 | + state.tx.unlock_time = tsx_data.unlock_time |
| 77 | + process_payment_id(state, tsx_data) |
| 78 | + await compute_sec_keys(state, tsx_data) |
| 79 | + gc.collect() |
| 80 | + |
| 81 | + # Iterative tx_prefix_hash hash computation |
| 82 | + state.tx_prefix_hasher.keep() |
| 83 | + state.tx_prefix_hasher.uvarint(state.tx.version) |
| 84 | + state.tx_prefix_hasher.uvarint(state.tx.unlock_time) |
| 85 | + state.tx_prefix_hasher.container_size(state.input_count) # ContainerType |
| 86 | + state.tx_prefix_hasher.release() |
| 87 | + state._mem_trace(10, True) |
| 88 | + |
| 89 | + # Final message hasher |
| 90 | + state.full_message_hasher.init(state.use_simple_rct) |
| 91 | + state.full_message_hasher.set_type_fee(state.get_rct_type(), state.get_fee()) |
| 92 | + |
| 93 | + # Sub address precomputation |
| 94 | + if tsx_data.account is not None and tsx_data.minor_indices: |
| 95 | + precompute_subaddr(state, tsx_data.account, tsx_data.minor_indices) |
| 96 | + state._mem_trace(5, True) |
| 97 | + |
| 98 | + # HMAC outputs - pinning |
| 99 | + hmacs = [] |
| 100 | + for idx in range(state.num_dests()): |
| 101 | + c_hmac = await hmac_encryption_keys.gen_hmac_tsxdest( |
| 102 | + state.key_hmac, tsx_data.outputs[idx], idx |
| 103 | + ) |
| 104 | + hmacs.append(c_hmac) |
| 105 | + gc.collect() |
| 106 | + |
| 107 | + state._mem_trace(6) |
| 108 | + |
| 109 | + from trezor.messages.MoneroTransactionInitAck import MoneroTransactionInitAck |
| 110 | + from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData |
| 111 | + |
| 112 | + rsig_data = MoneroTransactionRsigData(offload_type=state.rsig_offload) |
| 113 | + |
| 114 | + result_msg = MoneroTransactionInitAck( |
| 115 | + in_memory=False, |
| 116 | + many_inputs=True, |
| 117 | + many_outputs=True, |
| 118 | + hmacs=hmacs, |
| 119 | + rsig_data=rsig_data, |
| 120 | + ) |
| 121 | + return await dispatch_and_forward(state, result_msg) |
| 122 | + |
| 123 | + |
| 124 | +async def dispatch_and_forward(state, result_msg): |
| 125 | + from trezor.messages import MessageType |
| 126 | + from apps.monero.protocol.signing import step_02_set_input |
| 127 | + |
| 128 | + await state.ctx.write(result_msg) |
| 129 | + del result_msg |
| 130 | + |
| 131 | + received_msg = await state.ctx.read((MessageType.MoneroTransactionSetInputRequest,)) |
| 132 | + |
| 133 | + return await step_02_set_input.set_input(state, received_msg.src_entr) |
| 134 | + |
| 135 | + |
| 136 | +def get_primary_change_address(state: State): |
| 137 | + """ |
| 138 | + Computes primary change address for the current account index |
| 139 | + """ |
| 140 | + D, C = monero.generate_sub_address_keys( |
| 141 | + state.creds.view_key_private, state.creds.spend_key_public, state.account_idx, 0 |
| 142 | + ) |
| 143 | + return misc.StdObj( |
| 144 | + view_public_key=crypto.encodepoint(C), spend_public_key=crypto.encodepoint(D) |
| 145 | + ) |
| 146 | + |
| 147 | + |
| 148 | +def check_change(state: State, outputs): |
| 149 | + """ |
| 150 | + Checks if the change address is among tx outputs and it is equal to our address. |
| 151 | + """ |
| 152 | + from apps.monero.xmr.sub.addr import addr_eq, get_change_addr_idx |
| 153 | + |
| 154 | + change_idx = get_change_addr_idx(outputs, state.output_change) |
| 155 | + |
| 156 | + change_addr = state.change_address() |
| 157 | + if change_addr is None: |
| 158 | + state._mem_trace("No change" if __debug__ else None) |
| 159 | + return |
| 160 | + |
| 161 | + if change_idx is None and state.output_change.amount == 0 and len(outputs) == 2: |
| 162 | + state._mem_trace("Sweep tsx" if __debug__ else None) |
| 163 | + return # sweep dummy tsx |
| 164 | + |
| 165 | + found = False |
| 166 | + for out in outputs: |
| 167 | + if addr_eq(out.addr, change_addr): |
| 168 | + found = True |
| 169 | + break |
| 170 | + |
| 171 | + if not found: |
| 172 | + raise misc.TrezorChangeAddressError("Change address not found in outputs") |
| 173 | + |
| 174 | + my_addr = get_primary_change_address(state) |
| 175 | + if not addr_eq(my_addr, change_addr): |
| 176 | + raise misc.TrezorChangeAddressError("Change address differs from ours") |
| 177 | + |
| 178 | + return True |
| 179 | + |
| 180 | + |
| 181 | +def process_payment_id(state: State, tsx_data): |
| 182 | + """ |
| 183 | + Payment id -> extra |
| 184 | + """ |
| 185 | + if common.is_empty(tsx_data.payment_id): |
| 186 | + return |
| 187 | + |
| 188 | + from apps.monero.xmr.sub import tsx_helper |
| 189 | + from trezor import utils |
| 190 | + |
| 191 | + if len(tsx_data.payment_id) == 8: |
| 192 | + view_key_pub_enc = tsx_helper.get_destination_view_key_pub( |
| 193 | + tsx_data.outputs, state.change_address() |
| 194 | + ) |
| 195 | + if view_key_pub_enc == crypto.NULL_KEY_ENC: |
| 196 | + raise ValueError( |
| 197 | + "Destinations have to have exactly one output to support encrypted payment ids" |
| 198 | + ) |
| 199 | + |
| 200 | + view_key_pub = crypto.decodepoint(view_key_pub_enc) |
| 201 | + payment_id_encr = tsx_helper.encrypt_payment_id( |
| 202 | + tsx_data.payment_id, view_key_pub, state.tx_priv |
| 203 | + ) |
| 204 | + |
| 205 | + extra_nonce = payment_id_encr |
| 206 | + extra_prefix = 1 |
| 207 | + |
| 208 | + elif len(tsx_data.payment_id) == 32: |
| 209 | + extra_nonce = tsx_data.payment_id |
| 210 | + extra_prefix = 0 |
| 211 | + |
| 212 | + else: |
| 213 | + raise ValueError("Payment ID size invalid") |
| 214 | + |
| 215 | + lextra = len(extra_nonce) |
| 216 | + if lextra >= 255: |
| 217 | + raise ValueError("Nonce could be 255 bytes max") |
| 218 | + |
| 219 | + extra_buff = bytearray(3 + lextra) |
| 220 | + extra_buff[0] = 2 |
| 221 | + extra_buff[1] = lextra + 1 |
| 222 | + extra_buff[2] = extra_prefix |
| 223 | + utils.memcpy(extra_buff, 3, extra_nonce, 0, lextra) |
| 224 | + state.tx.extra = extra_buff |
| 225 | + |
| 226 | + |
| 227 | +async def compute_sec_keys(state: State, tsx_data): |
| 228 | + """ |
| 229 | + Generate master key H(TsxData || r) |
| 230 | + :return: |
| 231 | + """ |
| 232 | + import protobuf |
| 233 | + from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer |
| 234 | + |
| 235 | + writer = get_keccak_writer() |
| 236 | + await protobuf.dump_message(writer, tsx_data) |
| 237 | + writer.write(crypto.encodeint(state.tx_priv)) |
| 238 | + |
| 239 | + state.key_master = crypto.keccak_2hash( |
| 240 | + writer.get_digest() + crypto.encodeint(crypto.random_scalar()) |
| 241 | + ) |
| 242 | + state.key_hmac = crypto.keccak_2hash(b"hmac" + state.key_master) |
| 243 | + state.key_enc = crypto.keccak_2hash(b"enc" + state.key_master) |
| 244 | + |
| 245 | + |
| 246 | +def precompute_subaddr(state, account, indices): |
| 247 | + """ |
| 248 | + Precomputes subaddresses for account (major) and list of indices (minors) |
| 249 | + Subaddresses have to be stored in encoded form - unique representation. |
| 250 | + Single point can have multiple extended coordinates representation - would not match during subaddress search. |
| 251 | + """ |
| 252 | + monero.compute_subaddresses(state.creds, account, indices, state.subaddresses) |
0 commit comments