This repository has been archived by the owner on May 28, 2019. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 206
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
xmr: refactor builder to seperate steps
- lot of work to be done, but the general idea will probably stay - the messages workflow works, but the signed tx was not accepted by daemon, so there is a bug somewhere - additional cleanup/refactoring is defintely needed
- Loading branch information
Showing
15 changed files
with
1,625 additions
and
1,630 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import gc | ||
from micropython import const | ||
|
||
from trezor import log | ||
|
||
from apps.monero.xmr import crypto | ||
|
||
|
||
class TprefixStub: | ||
__slots__ = ("version", "unlock_time", "vin", "vout", "extra") | ||
|
||
def __init__(self, **kwargs): | ||
for kw in kwargs: | ||
setattr(self, kw, kwargs[kw]) | ||
|
||
|
||
class State: | ||
|
||
STEP_INP = const(100) | ||
STEP_PERM = const(200) | ||
STEP_VINI = const(300) | ||
STEP_ALL_IN = const(350) | ||
STEP_OUT = const(400) | ||
STEP_ALL_OUT = const(500) | ||
STEP_MLSAG = const(600) | ||
STEP_SIGN = const(700) | ||
|
||
def __init__(self, ctx): | ||
self.ctx = ctx | ||
self.creds = None | ||
self.key_master = None | ||
self.key_hmac = None | ||
self.key_enc = None | ||
|
||
self.tx_priv = None # txkey | ||
self.tx_pub = None | ||
|
||
self.multi_sig = False | ||
self.need_additional_txkeys = False | ||
self.use_bulletproof = False | ||
self.use_rct = True | ||
self.use_simple_rct = False | ||
self.input_count = 0 | ||
self.output_count = 0 | ||
self.output_change = None | ||
self.mixin = 0 | ||
self.fee = 0 | ||
self.account_idx = 0 | ||
|
||
self.additional_tx_private_keys = [] | ||
self.additional_tx_public_keys = [] | ||
self.inp_idx = -1 | ||
self.out_idx = -1 | ||
self.summary_inputs_money = 0 | ||
self.summary_outs_money = 0 | ||
self.input_secrets = [] | ||
self.input_alphas = [] | ||
self.input_pseudo_outs = [] | ||
self.output_sk = [] | ||
self.output_pk = [] | ||
self.output_amounts = [] | ||
self.output_masks = [] | ||
self.rsig_type = 0 | ||
self.rsig_grp = [] | ||
self.rsig_offload = 0 | ||
self.sumout = crypto.sc_0() | ||
self.sumpouts_alphas = crypto.sc_0() | ||
self.subaddresses = {} | ||
self.tx = None | ||
self.source_permutation = [] # sorted by key images | ||
self.tx_prefix_hasher = None | ||
self.tx_prefix_hash = None | ||
self.full_message_hasher = None | ||
self.full_message = None | ||
self.exp_tx_prefix_hash = None | ||
self._init() | ||
|
||
def _init(self): | ||
from apps.monero.xmr.sub.keccak_hasher import KeccakXmrArchive | ||
from apps.monero.xmr.sub.mlsag_hasher import PreMlsagHasher | ||
|
||
self.tx = TprefixStub(vin=[], vout=[], extra=b"") | ||
self.tx_prefix_hasher = KeccakXmrArchive() | ||
self.full_message_hasher = PreMlsagHasher() | ||
|
||
def _mem_trace(self, x=None, collect=False): | ||
if __debug__: | ||
log.debug( | ||
__name__, | ||
"Log trace: %s, ... F: %s A: %s", | ||
x, | ||
gc.mem_free(), | ||
gc.mem_alloc(), | ||
) | ||
if collect: | ||
gc.collect() | ||
|
||
def assrt(self, condition, msg=None): | ||
if condition: | ||
return | ||
raise ValueError("Assertion error%s" % (" : %s" % msg if msg else "")) | ||
|
||
def num_inputs(self): | ||
return self.input_count | ||
|
||
def num_dests(self): | ||
return self.output_count | ||
|
||
def get_fee(self): | ||
return self.fee if self.fee > 0 else 0 | ||
|
||
def change_address(self): | ||
return self.output_change.addr if self.output_change else None | ||
|
||
def get_rct_type(self): | ||
""" | ||
RCTsig type (simple/full x Borromean/Bulletproof) | ||
:return: | ||
""" | ||
from apps.monero.xmr.serialize_messages.tx_rsig import RctType | ||
|
||
if self.use_simple_rct: | ||
return RctType.FullBulletproof if self.use_bulletproof else RctType.Simple | ||
else: | ||
return RctType.Full |
252 changes: 252 additions & 0 deletions
252
src/apps/monero/protocol/signing/step_01_init_transaction.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
""" | ||
Initializes a new transaction. | ||
""" | ||
|
||
import gc | ||
|
||
from apps.monero.controller import misc | ||
from apps.monero.layout import confirms | ||
from apps.monero.protocol.signing.state import State | ||
from apps.monero.xmr import common, crypto, monero | ||
|
||
|
||
async def init_transaction(ctx, address_n, network_type, tsx_data): | ||
from apps.monero.xmr.sub.addr import classify_subaddresses | ||
from apps.monero.protocol import hmac_encryption_keys | ||
|
||
state = State(ctx) | ||
state.creds = await misc.monero_get_creds(ctx, address_n, network_type) | ||
|
||
state.tx_priv = crypto.random_scalar() | ||
state.tx_pub = crypto.scalarmult_base(state.tx_priv) | ||
|
||
state._mem_trace(1) | ||
|
||
# Ask for confirmation | ||
await confirms.confirm_transaction(state.ctx, tsx_data, state.creds) | ||
gc.collect() | ||
state._mem_trace(3) | ||
|
||
# Basic transaction parameters | ||
state.input_count = tsx_data.num_inputs | ||
state.output_count = len(tsx_data.outputs) | ||
state.output_change = misc.dst_entry_to_stdobj(tsx_data.change_dts) | ||
state.mixin = tsx_data.mixin | ||
state.fee = tsx_data.fee | ||
state.account_idx = tsx_data.account | ||
state.multi_sig = tsx_data.is_multisig | ||
check_change(state, tsx_data.outputs) | ||
state.exp_tx_prefix_hash = tsx_data.exp_tx_prefix_hash | ||
|
||
# Rsig data | ||
state.rsig_type = tsx_data.rsig_data.rsig_type | ||
state.rsig_grp = tsx_data.rsig_data.grouping | ||
state.rsig_offload = state.rsig_type > 0 and state.output_count > 2 | ||
state.use_bulletproof = state.rsig_type > 0 | ||
state.use_simple_rct = state.input_count > 1 or state.rsig_type != 0 | ||
|
||
# Provided tx key, used mostly in multisig. | ||
if len(tsx_data.use_tx_keys) > 0: | ||
for ckey in tsx_data.use_tx_keys: | ||
crypto.check_sc(crypto.decodeint(ckey)) | ||
|
||
state.tx_priv = crypto.decodeint(tsx_data.use_tx_keys[0]) | ||
state.tx_pub = crypto.scalarmult_base(state.tx_priv) | ||
state.additional_tx_private_keys = [ | ||
crypto.decodeint(x) for x in tsx_data.use_tx_keys[1:] | ||
] | ||
|
||
# Additional keys w.r.t. subaddress destinations | ||
class_res = classify_subaddresses(tsx_data.outputs, state.change_address()) | ||
num_stdaddresses, num_subaddresses, single_dest_subaddress = class_res | ||
|
||
# if this is a single-destination transfer to a subaddress, we set the tx pubkey to R=r*D | ||
if num_stdaddresses == 0 and num_subaddresses == 1: | ||
state.tx_pub = crypto.scalarmult( | ||
crypto.decodepoint(single_dest_subaddress.spend_public_key), state.tx_priv | ||
) | ||
|
||
state.need_additional_txkeys = num_subaddresses > 0 and ( | ||
num_stdaddresses > 0 or num_subaddresses > 1 | ||
) | ||
state._mem_trace(4, True) | ||
|
||
# Extra processing, payment id | ||
state.tx.version = 2 | ||
state.tx.unlock_time = tsx_data.unlock_time | ||
process_payment_id(state, tsx_data) | ||
await compute_sec_keys(state, tsx_data) | ||
gc.collect() | ||
|
||
# Iterative tx_prefix_hash hash computation | ||
state.tx_prefix_hasher.keep() | ||
state.tx_prefix_hasher.uvarint(state.tx.version) | ||
state.tx_prefix_hasher.uvarint(state.tx.unlock_time) | ||
state.tx_prefix_hasher.container_size(state.input_count) # ContainerType | ||
state.tx_prefix_hasher.release() | ||
state._mem_trace(10, True) | ||
|
||
# Final message hasher | ||
state.full_message_hasher.init(state.use_simple_rct) | ||
state.full_message_hasher.set_type_fee(state.get_rct_type(), state.get_fee()) | ||
|
||
# Sub address precomputation | ||
if tsx_data.account is not None and tsx_data.minor_indices: | ||
precompute_subaddr(state, tsx_data.account, tsx_data.minor_indices) | ||
state._mem_trace(5, True) | ||
|
||
# HMAC outputs - pinning | ||
hmacs = [] | ||
for idx in range(state.num_dests()): | ||
c_hmac = await hmac_encryption_keys.gen_hmac_tsxdest( | ||
state.key_hmac, tsx_data.outputs[idx], idx | ||
) | ||
hmacs.append(c_hmac) | ||
gc.collect() | ||
|
||
state._mem_trace(6) | ||
|
||
from trezor.messages.MoneroTransactionInitAck import MoneroTransactionInitAck | ||
from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData | ||
|
||
rsig_data = MoneroTransactionRsigData(offload_type=state.rsig_offload) | ||
|
||
result_msg = MoneroTransactionInitAck( | ||
in_memory=False, | ||
many_inputs=True, | ||
many_outputs=True, | ||
hmacs=hmacs, | ||
rsig_data=rsig_data, | ||
) | ||
return await dispatch_and_forward(state, result_msg) | ||
|
||
|
||
async def dispatch_and_forward(state, result_msg): | ||
from trezor.messages import MessageType | ||
from apps.monero.protocol.signing import step_02_set_input | ||
|
||
await state.ctx.write(result_msg) | ||
del result_msg | ||
|
||
received_msg = await state.ctx.read((MessageType.MoneroTransactionSetInputRequest,)) | ||
|
||
return await step_02_set_input.set_input(state, received_msg.src_entr) | ||
|
||
|
||
def get_primary_change_address(state: State): | ||
""" | ||
Computes primary change address for the current account index | ||
""" | ||
D, C = monero.generate_sub_address_keys( | ||
state.creds.view_key_private, state.creds.spend_key_public, state.account_idx, 0 | ||
) | ||
return misc.StdObj( | ||
view_public_key=crypto.encodepoint(C), spend_public_key=crypto.encodepoint(D) | ||
) | ||
|
||
|
||
def check_change(state: State, outputs): | ||
""" | ||
Checks if the change address is among tx outputs and it is equal to our address. | ||
""" | ||
from apps.monero.xmr.sub.addr import addr_eq, get_change_addr_idx | ||
|
||
change_idx = get_change_addr_idx(outputs, state.output_change) | ||
|
||
change_addr = state.change_address() | ||
if change_addr is None: | ||
state._mem_trace("No change" if __debug__ else None) | ||
return | ||
|
||
if change_idx is None and state.output_change.amount == 0 and len(outputs) == 2: | ||
state._mem_trace("Sweep tsx" if __debug__ else None) | ||
return # sweep dummy tsx | ||
|
||
found = False | ||
for out in outputs: | ||
if addr_eq(out.addr, change_addr): | ||
found = True | ||
break | ||
|
||
if not found: | ||
raise misc.TrezorChangeAddressError("Change address not found in outputs") | ||
|
||
my_addr = get_primary_change_address(state) | ||
if not addr_eq(my_addr, change_addr): | ||
raise misc.TrezorChangeAddressError("Change address differs from ours") | ||
|
||
return True | ||
|
||
|
||
def process_payment_id(state: State, tsx_data): | ||
""" | ||
Payment id -> extra | ||
""" | ||
if common.is_empty(tsx_data.payment_id): | ||
return | ||
|
||
from apps.monero.xmr.sub import tsx_helper | ||
from trezor import utils | ||
|
||
if len(tsx_data.payment_id) == 8: | ||
view_key_pub_enc = tsx_helper.get_destination_view_key_pub( | ||
tsx_data.outputs, state.change_address() | ||
) | ||
if view_key_pub_enc == crypto.NULL_KEY_ENC: | ||
raise ValueError( | ||
"Destinations have to have exactly one output to support encrypted payment ids" | ||
) | ||
|
||
view_key_pub = crypto.decodepoint(view_key_pub_enc) | ||
payment_id_encr = tsx_helper.encrypt_payment_id( | ||
tsx_data.payment_id, view_key_pub, state.tx_priv | ||
) | ||
|
||
extra_nonce = payment_id_encr | ||
extra_prefix = 1 | ||
|
||
elif len(tsx_data.payment_id) == 32: | ||
extra_nonce = tsx_data.payment_id | ||
extra_prefix = 0 | ||
|
||
else: | ||
raise ValueError("Payment ID size invalid") | ||
|
||
lextra = len(extra_nonce) | ||
if lextra >= 255: | ||
raise ValueError("Nonce could be 255 bytes max") | ||
|
||
extra_buff = bytearray(3 + lextra) | ||
extra_buff[0] = 2 | ||
extra_buff[1] = lextra + 1 | ||
extra_buff[2] = extra_prefix | ||
utils.memcpy(extra_buff, 3, extra_nonce, 0, lextra) | ||
state.tx.extra = extra_buff | ||
|
||
|
||
async def compute_sec_keys(state: State, tsx_data): | ||
""" | ||
Generate master key H(TsxData || r) | ||
:return: | ||
""" | ||
import protobuf | ||
from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer | ||
|
||
writer = get_keccak_writer() | ||
await protobuf.dump_message(writer, tsx_data) | ||
writer.write(crypto.encodeint(state.tx_priv)) | ||
|
||
state.key_master = crypto.keccak_2hash( | ||
writer.get_digest() + crypto.encodeint(crypto.random_scalar()) | ||
) | ||
state.key_hmac = crypto.keccak_2hash(b"hmac" + state.key_master) | ||
state.key_enc = crypto.keccak_2hash(b"enc" + state.key_master) | ||
|
||
|
||
def precompute_subaddr(state, account, indices): | ||
""" | ||
Precomputes subaddresses for account (major) and list of indices (minors) | ||
Subaddresses have to be stored in encoded form - unique representation. | ||
Single point can have multiple extended coordinates representation - would not match during subaddress search. | ||
""" | ||
monero.compute_subaddresses(state.creds, account, indices, state.subaddresses) |
Oops, something went wrong.