@ -10,53 +10,60 @@ from .state import State
from apps . monero import signing
from apps . monero . layout import confirms
from apps . monero . signing import RsigType, offloading_keys
from apps . monero . signing import offloading_keys
from apps . monero . xmr import crypto , serialize
async def set_output ( state : State , dst_entr , dst_entr_hmac , rsig_data ) :
async def set_output (
state : State , dst_entr , dst_entr_hmac , rsig_data , is_offloaded_bp = False
) :
state . mem_trace ( 0 , True )
mods = utils . unimport_begin ( )
await confirms . transaction_step (
state . ctx , state . STEP_OUT , state . current_output_index + 1 , state . output_count
)
state . mem_trace ( 1 )
# Progress update only for master message (skip for offloaded BP msg)
if not is_offloaded_bp :
await confirms . transaction_step (
state . ctx ,
state . STEP_OUT ,
state . current_output_index + 1 ,
state . output_count ,
)
state . current_output_index + = 1
state . mem_trace ( 1 , True )
dst_entr = await _validate ( state , dst_entr , dst_entr_hmac , is_offloaded_bp )
state . mem_trace ( 2 , True )
await _validate ( state , dst_entr , dst_entr_hmac )
# First output - we include the size of the container into the tx prefix hasher
if state . current_output_index == 0 :
state . tx_prefix_hasher . uvarint ( state . output_count )
state . mem_trace ( 4 , True )
if not state . is_processing_offloaded :
# First output - we include the size of the container into the tx prefix hasher
if state . current_output_index == 0 :
state . tx_prefix_hasher . uvarint ( state . output_count )
state . mem_trace ( 4 , True )
state . output_amounts . append ( dst_entr . amount )
state . summary_outs_money + = dst_entr . amount
state . output_amounts . append ( dst_entr . amount )
state . summary_outs_money + = dst_entr . amount
utils . unimport_end ( mods )
state . mem_trace ( 5 , True )
# Range proof first, memory intensiv e
rsig, mask = _range_proof ( state , dst_entr . amount , rsig_data )
# Compute tx keys and masks if applicabl e
tx_out_key, amount_key = _compute_tx_keys ( state , dst_entr )
utils . unimport_end ( mods )
state . mem_trace ( 6 , True )
# additional tx key if applicable
additional_txkey_priv = _set_out_additional_keys ( state , dst_entr )
# derivation = a*R or r*A or s*C
derivation = _set_out_derivation ( state , dst_entr , additional_txkey_priv )
# amount key = H_s(derivation || i)
amount_key = crypto . derivation_to_scalar ( derivation , state . current_output_index )
# one-time destination address P = H_s(derivation || i)*G + B
tx_out_key = crypto . derive_public_key (
derivation ,
state . current_output_index ,
crypto . decodepoint ( dst_entr . addr . spend_public_key ) ,
)
del ( derivation , additional_txkey_priv )
# Range proof first, memory intensive (fragmentation)
rsig_data_new , mask = _range_proof ( state , rsig_data )
utils . unimport_end ( mods )
state . mem_trace ( 7 , True )
# If det masks & offloading, return as we are handling offloaded BP.
if state . is_processing_offloaded :
from trezor . messages . MoneroTransactionSetOutputAck import (
MoneroTransactionSetOutputAck ,
)
return MoneroTransactionSetOutputAck ( )
# Tx header prefix hashing, hmac dst_entr
tx_out_bin , hmac_vouti = await _set_out_tx_out ( state , dst_entr , tx_out_key )
state . mem_trace ( 11 , True )
@ -93,29 +100,102 @@ async def set_output(state: State, dst_entr, dst_entr_hmac, rsig_data):
return MoneroTransactionSetOutputAck (
tx_out = tx_out_bin ,
vouti_hmac = hmac_vouti ,
rsig_data = _return_rsig_data( rsig ) ,
rsig_data = rsig_data_new ,
out_pk = out_pk_bin ,
ecdh_info = ecdh_info_bin ,
)
async def _validate ( state : State , dst_entr , dst_entr_hmac ) :
if state . current_input_index + 1 != state . input_count :
raise ValueError ( " Invalid number of inputs " )
if state . current_output_index > = state . output_count :
raise ValueError ( " Invalid output index " )
if dst_entr . amount < 0 :
raise ValueError ( " Destination with wrong amount: %s " % dst_entr . amount )
async def _validate ( state : State , dst_entr , dst_entr_hmac , is_offloaded_bp ) :
# If offloading flag then it has to be det_masks and offloading enabled.
# Using IF as it is easier to read.
if is_offloaded_bp and ( not state . rsig_offload or not state . is_det_mask ( ) ) :
raise ValueError ( " Extraneous offloaded msg " )
# HMAC check of the destination
dst_entr_hmac_computed = await offloading_keys . gen_hmac_tsxdest (
state . key_hmac , dst_entr , state . current_output_index
# State change according to the det-mask BP offloading.
if state . is_det_mask ( ) and state . rsig_offload :
bidx = _get_rsig_batch ( state , state . current_output_index )
last_in_batch = _is_last_in_batch ( state , state . current_output_index , bidx )
utils . ensure (
not last_in_batch or state . is_processing_offloaded != is_offloaded_bp ,
" Offloaded BP out of order " ,
)
state . is_processing_offloaded = is_offloaded_bp
if not state . is_processing_offloaded :
state . current_output_index + = 1
utils . ensure (
not dst_entr or dst_entr . amount > = 0 , " Destination with negative amount "
)
utils . ensure (
state . current_input_index + 1 == state . input_count , " Invalid number of inputs "
)
utils . ensure (
state . current_output_index < state . output_count , " Invalid output index "
)
if not crypto . ct_equals ( dst_entr_hmac , dst_entr_hmac_computed ) :
raise ValueError ( " HMAC invalid " )
del ( dst_entr_hmac , dst_entr_hmac_computed )
utils . ensure (
state . is_det_mask ( ) or not state . is_processing_offloaded ,
" Offloaded extra msg while not using det masks " ,
)
if not state . is_processing_offloaded :
# HMAC check of the destination
dst_entr_hmac_computed = await offloading_keys . gen_hmac_tsxdest (
state . key_hmac , dst_entr , state . current_output_index
)
utils . ensure (
crypto . ct_equals ( dst_entr_hmac , dst_entr_hmac_computed ) , " HMAC failed "
)
del ( dst_entr_hmac_computed )
else :
dst_entr = None
del ( dst_entr_hmac )
state . mem_trace ( 3 , True )
return dst_entr
def _compute_tx_keys ( state : State , dst_entr ) :
""" Computes tx_out_key, amount_key """
if state . is_processing_offloaded :
return None , None # no need to recompute
# additional tx key if applicable
additional_txkey_priv = _set_out_additional_keys ( state , dst_entr )
# derivation = a*R or r*A or s*C
derivation = _set_out_derivation ( state , dst_entr , additional_txkey_priv )
# amount key = H_s(derivation || i)
amount_key = crypto . derivation_to_scalar ( derivation , state . current_output_index )
# one-time destination address P = H_s(derivation || i)*G + B
tx_out_key = crypto . derive_public_key (
derivation ,
state . current_output_index ,
crypto . decodepoint ( dst_entr . addr . spend_public_key ) ,
)
del ( derivation , additional_txkey_priv )
# Computes the newest mask if applicable
if state . is_det_mask ( ) :
from apps . monero . xmr import monero
mask = monero . commitment_mask ( crypto . encodeint ( amount_key ) )
elif state . current_output_index + 1 < state . output_count :
mask = offloading_keys . det_comm_masks ( state . key_enc , state . current_output_index )
else :
mask = state . output_last_mask
state . output_last_mask = None
state . output_masks . append ( mask )
return tx_out_key , amount_key
async def _set_out_tx_out ( state : State , dst_entr , tx_out_key ) :
"""
@ -139,109 +219,128 @@ async def _set_out_tx_out(state: State, dst_entr, tx_out_key):
return tx_out_bin , hmac_vouti
def _range_proof ( state , amount, rsig_data) :
def _range_proof ( state , rsig_data) :
"""
Computes rangeproof
In order to optimize incremental transaction build , the mask computation is changed compared
to the official Monero code . In the official code , the input pedersen commitments are computed
after range proof in such a way summed masks for commitments ( alpha ) and rangeproofs ( ai ) are equal .
In order to save roundtrips we compute commitments randomly and then for the last rangeproof
a [ 63 ] = ( \\sum_ { i = 0 } ^ { num_inp } alpha_i - \\sum_ { i = 0 } ^ { num_outs - 1 } amasks_i ) - \\sum_ { i = 0 } ^ { 62 } a_i
Computes rangeproof and handles range proof offloading logic .
Since HF10 the commitments are deterministic .
The range proof is incrementally hashed to the final_message .
"""
from apps . monero . xmr import range_signatures
mask = state . output_masks [ state . current_output_index ]
provided_rsig = None
if rsig_data and rsig_data . rsig and len ( rsig_data . rsig ) > 0 :
provided_rsig = rsig_data . rsig
if not state . rsig_offload and provided_rsig :
raise signing . Error ( " Provided unexpected rsig " )
# Batching
# Batching & validation
bidx = _get_rsig_batch ( state , state . current_output_index )
batch_size = state . rsig_grouping [ bidx ]
last_in_batch = _is_last_in_batch ( state , state . current_output_index , bidx )
if state . rsig_offload and provided_rsig and not last_in_batch :
raise signing . Error ( " Provided rsig too early " )
if state . rsig_offload and last_in_batch and not provided_rsig :
if (
state . rsig_offload
and last_in_batch
and not provided_rsig
and ( not state . is_det_mask ( ) or state . is_processing_offloaded )
) :
raise signing . Error ( " Rsig expected, not provided " )
# Batch not finished, skip range sig generation now
mask = state . output_masks [ - 1 ] if not state . is_processing_offloaded else None
offload_mask = mask and state . is_det_mask ( ) and state . rsig_offload
# If not last, do not proceed to the BP processing.
if not last_in_batch :
return None , mask
rsig_data_new = (
_return_rsig_data ( mask = crypto . encodeint ( mask ) ) if offload_mask else None
)
return rsig_data_new , mask
# Rangeproof
# Pedersen commitment on the value, mask from the commitment, range signature.
C, rsig = None , None
rsig = None
state . mem_trace ( " pre-rproof " if __debug__ else None , collect = True )
if state . rsig_type == RsigType . Bulletproof and not state . rsig_offload :
""" Bulletproof calculation in trezor """
rsig = range_signatures . prove_range_bp_batch (
state . output_amounts , state . output_masks
)
state . mem_trace ( " post-bp " if __debug__ else None , collect = True )
# Incremental BP hashing
# BP is hashed with raw=False as hash does not contain L, R
# array sizes compared to the serialized bulletproof format
# thus direct serialization cannot be used.
state . full_message_hasher . rsig_val ( rsig , True , raw = False )
state . mem_trace ( " post-bp-hash " if __debug__ else None , collect = True )
rsig = _dump_rsig_bp ( rsig )
state . mem_trace (
" post-bp-ser, size: %s " % len ( rsig ) if __debug__ else None , collect = True
)
if not state . rsig_offload :
""" Bulletproof calculation in Trezor """
rsig = _rsig_bp ( state )
elif state . rsig_type == RsigType . Borromean and not state . rsig_offload :
""" Borromean calculation in trezor """
C , mask , rsig = range_signatures . prove_range_borromean ( amount , mask )
del range_signatures
# Incremental hashing
state . full_message_hasher . rsig_val ( rsig , False , raw = True )
_check_out_commitment ( state , amount , mask , C )
elif state . rsig_type == RsigType . Bulletproof and state . rsig_offload :
""" Bulletproof calculated on host, verify in trezor """
from apps . monero . xmr . serialize_messages . tx_rsig_bulletproof import Bulletproof
# TODO this should be tested
# last_in_batch = True (see above) so this is fine
masks = state . output_masks [
1 + state . current_output_index - batch_size : 1 + state . current_output_index
]
bp_obj = serialize . parse_msg ( rsig_data . rsig , Bulletproof )
rsig_data . rsig = None
# BP is hashed with raw=False as hash does not contain L, R
# array sizes compared to the serialized bulletproof format
# thus direct serialization cannot be used.
state . full_message_hasher . rsig_val ( bp_obj , True , raw = False )
res = range_signatures . verify_bp ( bp_obj , state . output_amounts , masks )
utils . ensure ( res , " BP verification fail " )
state . mem_trace ( " BP verified " if __debug__ else None , collect = True )
del ( bp_obj , range_signatures )
elif state . rsig_type == RsigType . Borromean and state . rsig_offload :
""" Borromean offloading not supported """
raise signing . Error (
" Unsupported rsig state (Borromean offloaded is not supported) "
)
elif state . is_det_mask ( ) and not state . is_processing_offloaded :
""" Bulletproof offloaded to the host, deterministic masks. Nothing here, waiting for offloaded BP. """
pass
elif state . is_det_mask ( ) and state . is_processing_offloaded :
""" Bulletproof offloaded to the host, check BP, hash it. """
_rsig_process_bp ( state , rsig_data )
else :
raise signing . Error ( " Unexpected rsig state " )
""" Bulletproof calculated on host, verify in Trezor """
_rsig_process_bp ( state , rsig_data )
state . mem_trace ( " rproof " if __debug__ else None , collect = True )
if state . current_output_index + 1 == state . output_count :
# Construct new rsig data to send back to the host.
rsig_data_new = _return_rsig_data (
rsig , crypto . encodeint ( mask ) if offload_mask else None
)
if state . current_output_index + 1 == state . output_count and (
not state . rsig_offload or state . is_processing_offloaded
) :
# output masks and amounts are not needed anymore
state . output_amounts = [ ]
state . output_masks = [ ]
return rsig , mask
state . output_amounts = None
state . output_masks = None
return rsig_data_new , mask
def _rsig_bp ( state : State ) :
""" Bulletproof calculation in trezor """
from apps . monero . xmr import range_signatures
rsig = range_signatures . prove_range_bp_batch (
state . output_amounts , state . output_masks
)
state . mem_trace ( " post-bp " if __debug__ else None , collect = True )
# Incremental BP hashing
# BP is hashed with raw=False as hash does not contain L, R
# array sizes compared to the serialized bulletproof format
# thus direct serialization cannot be used.
state . full_message_hasher . rsig_val ( rsig , True , raw = False )
state . mem_trace ( " post-bp-hash " if __debug__ else None , collect = True )
rsig = _dump_rsig_bp ( rsig )
state . mem_trace (
" post-bp-ser, size: %s " % len ( rsig ) if __debug__ else None , collect = True
)
# state cleanup
state . output_masks = [ ]
state . output_amounts = [ ]
return rsig
def _rsig_process_bp ( state : State , rsig_data ) :
from apps . monero . xmr import range_signatures
from apps . monero . xmr . serialize_messages . tx_rsig_bulletproof import Bulletproof
bp_obj = serialize . parse_msg ( rsig_data . rsig , Bulletproof )
rsig_data . rsig = None
# BP is hashed with raw=False as hash does not contain L, R
# array sizes compared to the serialized bulletproof format
# thus direct serialization cannot be used.
state . full_message_hasher . rsig_val ( bp_obj , True , raw = False )
res = range_signatures . verify_bp ( bp_obj , state . output_amounts , state . output_masks )
utils . ensure ( res , " BP verification fail " )
state . mem_trace ( " BP verified " if __debug__ else None , collect = True )
del ( bp_obj , range_signatures )
# State cleanup after verification is finished
state . output_amounts = [ ]
state . output_masks = [ ]
def _dump_rsig_bp ( rsig ) :
@ -286,15 +385,21 @@ def _dump_rsig_bp(rsig):
return buff
def _return_rsig_data ( rsig ) :
if rsig is None :
def _return_rsig_data ( rsig = None , mask = None ) :
if rsig is None and mask is None :
return None
from trezor . messages . MoneroTransactionRsigData import MoneroTransactionRsigData
if isinstance ( rsig , list ) :
return MoneroTransactionRsigData ( rsig_parts = rsig )
else :
return MoneroTransactionRsigData ( rsig = rsig )
rsig_data = MoneroTransactionRsigData ( )
if mask :
rsig_data . mask = mask
if rsig :
rsig_data . rsig = rsig
return rsig_data
def _get_ecdh_info_and_out_pk ( state : State , tx_out_key , amount , mask , amount_key ) :
@ -305,23 +410,49 @@ def _get_ecdh_info_and_out_pk(state: State, tx_out_key, amount, mask, amount_key
"""
out_pk_dest = crypto . encodepoint ( tx_out_key )
out_pk_commitment = crypto . encodepoint ( crypto . gen_commitment ( mask , amount ) )
state . sumout = crypto . sc_add ( state . sumout , mask )
state . output_sk_masks . append ( mask )
crypto . sc_add_into ( state . sumout , state . sumout , mask )
# masking of mask and amount
ecdh_info = _ecdh_encode ( mask , amount , crypto . encodeint ( amount_key ) )
ecdh_info = _ecdh_encode (
mask , amount , crypto . encodeint ( amount_key ) , state . is_bulletproof_v2 ( )
)
# Manual ECDH info serialization
ecdh_info_bin = bytearray ( 64 )
utils . memcpy ( ecdh_info_bin , 0 , ecdh_info . mask , 0 , 32 )
utils . memcpy ( ecdh_info_bin , 32 , ecdh_info . amount , 0 , 32 )
ecdh_info_bin = _serialize_ecdh ( ecdh_info , state . is_bulletproof_v2 ( ) )
gc . collect ( )
return out_pk_dest , out_pk_commitment , ecdh_info_bin
def _ecdh_encode ( mask , amount , amount_key ) :
def _serialize_ecdh ( ecdh_info , v2 = False ) :
"""
Serializes ECDH according to the current format defined by the hard fork version
or the signature format respectively .
"""
if v2 :
# In HF10 the amount is serialized to 8B and mask is deterministic
ecdh_info_bin = bytearray ( 8 )
ecdh_info_bin [ : ] = ecdh_info . amount [ 0 : 8 ]
return ecdh_info_bin
else :
ecdh_info_bin = bytearray ( 64 )
utils . memcpy ( ecdh_info_bin , 0 , ecdh_info . mask , 0 , 32 )
utils . memcpy ( ecdh_info_bin , 32 , ecdh_info . amount , 0 , 32 )
return ecdh_info_bin
def _ecdh_hash ( shared_sec ) :
"""
Generates ECDH hash for amount masking for Bulletproof2
"""
data = bytearray ( 38 )
data [ 0 : 6 ] = b " amount "
data [ 6 : ] = shared_sec
return crypto . cn_fast_hash ( data )
def _ecdh_encode ( mask , amount , amount_key , v2 = False ) :
"""
Output recipients need be able to reconstruct the amount commitments .
This means the blinding factor ` mask ` and ` amount ` must be communicated
@ -336,23 +467,27 @@ def _ecdh_encode(mask, amount, amount_key):
from apps . monero . xmr . serialize_messages . tx_ecdh import EcdhTuple
ecdh_info = EcdhTuple ( mask = mask , amount = crypto . sc_init ( amount ) )
amount_key_hash_single = crypto . hash_to_scalar ( amount_key )
amount_key_hash_double = crypto . hash_to_scalar (
crypto . encodeint ( amount_key_hash_single )
)
ecdh_info . mask = crypto . sc_add ( ecdh_info . mask , amount_key_hash_single )
ecdh_info . amount = crypto . sc_add ( ecdh_info . amount , amount_key_hash_double )
return _recode_ecdh ( ecdh_info )
if v2 :
amnt = ecdh_info . amount
ecdh_info . mask = crypto . NULL_KEY_ENC
ecdh_info . amount = bytearray ( 32 )
crypto . encodeint_into ( ecdh_info . amount , amnt )
crypto . xor8 ( ecdh_info . amount , _ecdh_hash ( amount_key ) )
return ecdh_info
else :
amount_key_hash_single = crypto . hash_to_scalar ( amount_key )
amount_key_hash_double = crypto . hash_to_scalar (
crypto . encodeint ( amount_key_hash_single )
)
def _recode_ecdh ( ecdh_info ) :
"""
In - place ecdh_info tuple recoding
"""
ecdh_info . mask = crypto . encodeint ( ecdh_info . mask )
ecdh_info . amount = crypto . encodeint ( ecdh_info . amount )
return ecdh_info
# Not modifying passed mask, is reused in BP.
ecdh_info . mask = crypto . sc_add ( ecdh_info . mask , amount_key_hash_single )
crypto . sc_add_into ( ecdh_info . amount , ecdh_info . amount , amount_key_hash_double )
ecdh_info . mask = crypto . encodeint ( ecdh_info . mask )
ecdh_info . amount = crypto . encodeint ( ecdh_info . amount )
return ecdh_info
def _set_out_additional_keys ( state : State , dst_entr ) :
@ -367,8 +502,9 @@ def _set_out_additional_keys(state: State, dst_entr):
if dst_entr . is_subaddress :
# R=r*D
additional_txkey = crypto . scalarmult (
crypto . decodepoint ( dst_entr . addr . spend_public_key ) , additional_txkey_priv
additional_txkey = crypto . decodepoint ( dst_entr . addr . spend_public_key )
crypto . scalarmult_into (
additional_txkey , additional_txkey , additional_txkey_priv
)
else :
# R=r*G
@ -410,16 +546,6 @@ def _set_out_derivation(state: State, dst_entr, additional_txkey_priv):
return derivation
def _check_out_commitment ( state : State , amount , mask , C ) :
utils . ensure (
crypto . point_eq (
C ,
crypto . point_add ( crypto . scalarmult_base ( mask ) , crypto . scalarmult_h ( amount ) ) ,
) ,
" OutC fail " ,
)
def _is_last_in_batch ( state : State , idx , bidx ) :
"""
Returns true if the current output is last in the rsig batch