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

Commit 6475133

Browse files
tsusankaph4r05
authored andcommitted
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
1 parent 14b0a85 commit 6475133

15 files changed

+1625
-1630
lines changed

src/apps/monero/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ to the agent/hot-wallet so it can decrypt computed KIs and import it
172172

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

175+
TODO
176+
175177
- The wrapping message: `MoneroTransactionSignRequest`.
176178
- The main multiplexor: `apps/monero/protocol/tsx_sign.py`
177179
- The main signing logic is implemented in `apps/monero/protocol/tsx_sign_builder.py`
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import gc
2+
from micropython import const
3+
4+
from trezor import log
5+
6+
from apps.monero.xmr import crypto
7+
8+
9+
class TprefixStub:
10+
__slots__ = ("version", "unlock_time", "vin", "vout", "extra")
11+
12+
def __init__(self, **kwargs):
13+
for kw in kwargs:
14+
setattr(self, kw, kwargs[kw])
15+
16+
17+
class State:
18+
19+
STEP_INP = const(100)
20+
STEP_PERM = const(200)
21+
STEP_VINI = const(300)
22+
STEP_ALL_IN = const(350)
23+
STEP_OUT = const(400)
24+
STEP_ALL_OUT = const(500)
25+
STEP_MLSAG = const(600)
26+
STEP_SIGN = const(700)
27+
28+
def __init__(self, ctx):
29+
self.ctx = ctx
30+
self.creds = None
31+
self.key_master = None
32+
self.key_hmac = None
33+
self.key_enc = None
34+
35+
self.tx_priv = None # txkey
36+
self.tx_pub = None
37+
38+
self.multi_sig = False
39+
self.need_additional_txkeys = False
40+
self.use_bulletproof = False
41+
self.use_rct = True
42+
self.use_simple_rct = False
43+
self.input_count = 0
44+
self.output_count = 0
45+
self.output_change = None
46+
self.mixin = 0
47+
self.fee = 0
48+
self.account_idx = 0
49+
50+
self.additional_tx_private_keys = []
51+
self.additional_tx_public_keys = []
52+
self.inp_idx = -1
53+
self.out_idx = -1
54+
self.summary_inputs_money = 0
55+
self.summary_outs_money = 0
56+
self.input_secrets = []
57+
self.input_alphas = []
58+
self.input_pseudo_outs = []
59+
self.output_sk = []
60+
self.output_pk = []
61+
self.output_amounts = []
62+
self.output_masks = []
63+
self.rsig_type = 0
64+
self.rsig_grp = []
65+
self.rsig_offload = 0
66+
self.sumout = crypto.sc_0()
67+
self.sumpouts_alphas = crypto.sc_0()
68+
self.subaddresses = {}
69+
self.tx = None
70+
self.source_permutation = [] # sorted by key images
71+
self.tx_prefix_hasher = None
72+
self.tx_prefix_hash = None
73+
self.full_message_hasher = None
74+
self.full_message = None
75+
self.exp_tx_prefix_hash = None
76+
self._init()
77+
78+
def _init(self):
79+
from apps.monero.xmr.sub.keccak_hasher import KeccakXmrArchive
80+
from apps.monero.xmr.sub.mlsag_hasher import PreMlsagHasher
81+
82+
self.tx = TprefixStub(vin=[], vout=[], extra=b"")
83+
self.tx_prefix_hasher = KeccakXmrArchive()
84+
self.full_message_hasher = PreMlsagHasher()
85+
86+
def _mem_trace(self, x=None, collect=False):
87+
if __debug__:
88+
log.debug(
89+
__name__,
90+
"Log trace: %s, ... F: %s A: %s",
91+
x,
92+
gc.mem_free(),
93+
gc.mem_alloc(),
94+
)
95+
if collect:
96+
gc.collect()
97+
98+
def assrt(self, condition, msg=None):
99+
if condition:
100+
return
101+
raise ValueError("Assertion error%s" % (" : %s" % msg if msg else ""))
102+
103+
def num_inputs(self):
104+
return self.input_count
105+
106+
def num_dests(self):
107+
return self.output_count
108+
109+
def get_fee(self):
110+
return self.fee if self.fee > 0 else 0
111+
112+
def change_address(self):
113+
return self.output_change.addr if self.output_change else None
114+
115+
def get_rct_type(self):
116+
"""
117+
RCTsig type (simple/full x Borromean/Bulletproof)
118+
:return:
119+
"""
120+
from apps.monero.xmr.serialize_messages.tx_rsig import RctType
121+
122+
if self.use_simple_rct:
123+
return RctType.FullBulletproof if self.use_bulletproof else RctType.Simple
124+
else:
125+
return RctType.Full
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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

Comments
 (0)