Skip to content
This repository has been archived by the owner on May 28, 2019. It is now read-only.

Commit

Permalink
xmr: refactor builder to seperate steps
Browse files Browse the repository at this point in the history
- 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
tsusanka authored and ph4r05 committed Sep 27, 2018
1 parent 14b0a85 commit 6475133
Show file tree
Hide file tree
Showing 15 changed files with 1,625 additions and 1,630 deletions.
2 changes: 2 additions & 0 deletions src/apps/monero/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ to the agent/hot-wallet so it can decrypt computed KIs and import it

For detailed description and rationale please refer to the [monero-doc].

TODO

- The wrapping message: `MoneroTransactionSignRequest`.
- The main multiplexor: `apps/monero/protocol/tsx_sign.py`
- The main signing logic is implemented in `apps/monero/protocol/tsx_sign_builder.py`
Expand Down
125 changes: 125 additions & 0 deletions src/apps/monero/protocol/signing/state.py
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 src/apps/monero/protocol/signing/step_01_init_transaction.py
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)
Loading

0 comments on commit 6475133

Please sign in to comment.