1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 14:28:07 +00:00

Merge pull request #478 from ph4r05/pr/hf10

xmr: hardfork 10 upgrade
This commit is contained in:
Tomas Susanka 2019-02-20 13:51:02 +01:00 committed by GitHub
commit 765d88c2b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 963 additions and 789 deletions

View File

@ -26,7 +26,7 @@ Pillow = ">=5.2.0"
Mako = ">=1.0.7"
# monero
monero_agent = {version = ">=1.7.1", extras = ["tcry", "dev"]}
monero_agent = {version = ">=2.0.1", extras = ["tcry", "dev"]}
py_trezor_crypto_ph4 = {version = ">=0.1.1"}
[dev-packages]

11
Pipfile.lock generated
View File

@ -290,17 +290,17 @@
"tcry"
],
"hashes": [
"sha256:229855aeffc2457c3cd20b30a41d8c31fc2898d9deb3667cfdb85ce5318aa218",
"sha256:898324657bf87c9f002dab5d2137565abda95950f52cbdbc6ccc659dd9d9910e"
"sha256:0e8ac7a9ff9512b9781deacf6a5ae8c53f49d0b8e43684278e4e57f328493ebb",
"sha256:e6e99d44a2d76cdc1addb9421d5039546d4ea0002691ca5186124aa4561f82f5"
],
"index": "pypi",
"version": "==1.7.6"
"version": "==2.0.1"
},
"monero-serialize": {
"hashes": [
"sha256:81ae31a25901cf81969b48a14f9267775b9dbe4856f322807732bf03836773c7"
"sha256:0bf42972a2b13b47c2b2bd42352006caf14117387c1fab09ab2ac0817c0dede5"
],
"version": "==2.1.0"
"version": "==3.0.0"
},
"more-itertools": {
"hashes": [
@ -433,6 +433,7 @@
"sha256:c523afd73949ac083bb7c5ef67416f0a971edc1921f4046a8cd40f87e43133ff"
],
"index": "pypi",
"markers": "extra == 'tcry'",
"version": "==0.1.1"
},
"pyblake2": {

View File

@ -332,8 +332,8 @@ COINS = [
maxfee_kb=2000000,
signed_message_header="Capricoin Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=0x049d7cb2,
xpub_magic_segwit_native=0x04b24746,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix=None,
cashaddr_prefix=None,
slip44=289,
@ -724,26 +724,6 @@ COINS = [
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="MonetaryUnit",
coin_shortcut="MUE",
address_type=16,
address_type_p2sh=76,
maxfee_kb=100000,
signed_message_header="MonetaryUnit Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix=None,
cashaddr_prefix=None,
slip44=31,
segwit=False,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Megacoin",
coin_shortcut="MEC",
@ -784,6 +764,26 @@ COINS = [
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="MonetaryUnit",
coin_shortcut="MUE",
address_type=16,
address_type_p2sh=76,
maxfee_kb=100000,
signed_message_header="MonetaryUnit Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix=None,
cashaddr_prefix=None,
slip44=31,
segwit=False,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Myriad",
coin_shortcut="XMY",
@ -804,6 +804,26 @@ COINS = [
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="NIX",
coin_shortcut="NIX",
address_type=38,
address_type_p2sh=53,
maxfee_kb=40000000,
signed_message_header="NIX Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=0x049d7cb2,
xpub_magic_segwit_native=None,
bech32_prefix="nix",
cashaddr_prefix=None,
slip44=400,
segwit=True,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Namecoin",
coin_shortcut="NMC",
@ -825,19 +845,39 @@ COINS = [
curve_name='secp256k1',
),
CoinInfo(
coin_name="Nix",
coin_shortcut="NIX",
address_type=38,
address_type_p2sh=53,
maxfee_kb=40000000,
signed_message_header="NIX Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=0x049d7cb2,
coin_name="PIVX",
coin_shortcut="PIVX",
address_type=30,
address_type_p2sh=13,
maxfee_kb=100000,
signed_message_header="DarkNet Signed Message:\n",
xpub_magic=0x022d2533,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix="nix",
bech32_prefix=None,
cashaddr_prefix=None,
slip44=400,
segwit=True,
slip44=119,
segwit=False,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="PIVX Testnet",
coin_shortcut="tPIVX",
address_type=139,
address_type_p2sh=19,
maxfee_kb=100000,
signed_message_header="DarkNet Signed Message:\n",
xpub_magic=0x3a8061a0,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix=None,
cashaddr_prefix=None,
slip44=1,
segwit=False,
fork_id=None,
force_bip143=False,
bip115=False,
@ -864,6 +904,26 @@ COINS = [
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Primecoin",
coin_shortcut="XPM",
address_type=23,
address_type_p2sh=83,
maxfee_kb=1000000,
signed_message_header="Primecoin Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix=None,
cashaddr_prefix=None,
slip44=24,
segwit=False,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Ravencoin",
coin_shortcut="RVN",
@ -924,6 +984,26 @@ COINS = [
decred=False,
curve_name='secp256k1-smart',
),
CoinInfo(
coin_name="Stakenet",
coin_shortcut="XSN",
address_type=76,
address_type_p2sh=16,
maxfee_kb=2000000,
signed_message_header="DarkCoin Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=0x049d7cb2,
xpub_magic_segwit_native=0x04b24746,
bech32_prefix="xc",
cashaddr_prefix=None,
slip44=199,
segwit=True,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Vertcoin",
coin_shortcut="VTC",
@ -964,6 +1044,26 @@ COINS = [
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="ZClassic",
coin_shortcut="ZCL",
address_type=7352,
address_type_p2sh=7357,
maxfee_kb=1000000,
signed_message_header="Zcash Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=None,
xpub_magic_segwit_native=None,
bech32_prefix=None,
cashaddr_prefix=None,
slip44=147,
segwit=False,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Zcash",
coin_shortcut="ZEC",

View File

@ -191,6 +191,20 @@ NETWORKS = [
name="Akroma",
rskip60=False,
),
NetworkInfo(
chain_id=246529,
slip44=246529,
shortcut="ATS",
name="ARTIS sigma1",
rskip60=False,
),
NetworkInfo(
chain_id=246785,
slip44=1,
shortcut="tATS",
name="ARTIS tau1",
rskip60=False,
),
NetworkInfo(
chain_id=1313114,
slip44=1313114,

File diff suppressed because it is too large Load Diff

View File

@ -64,7 +64,11 @@ async def require_confirm_transaction(ctx, tsx_data, network_type):
cur_payment = None
await _require_confirm_output(ctx, dst, network_type, cur_payment)
if has_payment and not has_integrated:
if (
has_payment
and not has_integrated
and tsx_data.payment_id != b"\x00\x00\x00\x00\x00\x00\x00\x00"
):
await _require_confirm_payment_id(ctx, tsx_data.payment_id)
await _require_confirm_fee(ctx, tsx_data.fee)

View File

@ -67,12 +67,7 @@ async def sign_tx_dispatch(state, msg, keychain):
return (
await step_04_input_vini.input_vini(
state,
msg.src_entr,
msg.vini,
msg.vini_hmac,
msg.pseudo_out,
msg.pseudo_out_hmac,
state, msg.src_entr, msg.vini, msg.vini_hmac
),
(
MessageType.MoneroTransactionInputViniRequest,
@ -91,11 +86,14 @@ async def sign_tx_dispatch(state, msg, keychain):
elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionSetOutputRequest:
from apps.monero.signing import step_06_set_output
is_offloaded_bp = bool(msg.is_offloaded_bp)
dst, dst_hmac, rsig_data = msg.dst_entr, msg.dst_entr_hmac, msg.rsig_data
del msg
return (
await step_06_set_output.set_output(state, dst, dst_hmac, rsig_data),
await step_06_set_output.set_output(
state, dst, dst_hmac, rsig_data, is_offloaded_bp
),
(
MessageType.MoneroTransactionSetOutputRequest,
MessageType.MoneroTransactionAllOutSetRequest,

View File

@ -43,18 +43,14 @@ class RsigType:
Bulletproof = 1
def get_monero_rct_type(rct_type, rsig_type):
def get_monero_rct_type(bp_version=1):
"""
This converts our internal representation of RctType and RsigType
into what is used in Monero:
- Null = 0
- Full = 1
- Simple = 2
- Simple/Full with bulletproof = 3
Returns transaction RctType according to the BP version.
Only HP9+ is supported, thus Full and Simple variants are removed.
"""
if rsig_type == RsigType.Bulletproof:
return 3 # Bulletproofs
if rct_type == RctType.Simple:
return 2 # Simple
if bp_version == 1:
return 3 # TxRctType.Bulletproof
elif bp_version == 2:
return 4 # TxRctType.Bulletproof2
else:
return 1 # Full
raise ValueError("Unsupported BP version")

View File

@ -2,14 +2,28 @@ from trezor import utils
from apps.monero.xmr import crypto
if False:
from apps.monero.xmr.types import *
def _build_key(secret, discriminator=None, index: int = None) -> bytes:
BUILD_KEY_BUFFER = bytearray(32 + 12 + 4) # key + disc + index
def _build_key(
secret, discriminator=None, index: int = None, out: bytes = None
) -> bytes:
"""
Creates an unique-purpose key
"""
key_buff = bytearray(32 + 12 + 4) # key + disc + index
key_buff = BUILD_KEY_BUFFER # bytearray(32 + 12 + 4) # key + disc + index
utils.ensure(len(secret) == 32, "Invalid key length")
utils.ensure(len(discriminator) <= 12, "Disc too long")
offset = 32
utils.memcpy(key_buff, 0, secret, 0, len(secret))
utils.memcpy(key_buff, 0, secret, 0, 32)
for i in range(32, len(key_buff)):
key_buff[i] = 0
if discriminator is not None:
utils.memcpy(key_buff, offset, discriminator, 0, len(discriminator))
@ -24,7 +38,7 @@ def _build_key(secret, discriminator=None, index: int = None) -> bytes:
offset += 1
index = shifted
return crypto.keccak_2hash(key_buff)
return crypto.keccak_2hash(key_buff, out)
def hmac_key_txin(key_hmac, idx: int) -> bytes:
@ -83,6 +97,13 @@ def enc_key_cout(key_enc, idx: int = None) -> bytes:
return _build_key(key_enc, b"cout", idx)
def det_comm_masks(key_enc, idx: int) -> Sc25519:
"""
Deterministic output commitment masks
"""
return crypto.decodeint(_build_key(key_enc, b"out-mask", idx))
async def gen_hmac_vini(key, src_entr, vini_bin, idx: int) -> bytes:
"""
Computes hmac (TxSourceEntry[i] || tx.vin[i])

View File

@ -60,12 +60,11 @@ class State:
"""
self.need_additional_txkeys = False
# Ring Confidential Transaction type
# allowed values: RctType.{Full, Simple}
self.rct_type = None
# Range Signature type (also called range proof)
# allowed values: RsigType.{Borromean, Bulletproof}
self.rsig_type = None
# Connected client version
self.client_version = 0
# Bulletproof version. Pre for <=HF9 is 1, for >HP10 is 2
self.bp_version = 1
self.input_count = 0
self.output_count = 0
@ -82,18 +81,22 @@ class State:
# currently processed input/output index
self.current_input_index = -1
self.current_output_index = -1
self.is_processing_offloaded = False
# for pseudo_out recomputation from new mask
self.input_last_amount = 0
self.summary_inputs_money = 0
self.summary_outs_money = 0
# output commitments
self.output_pk_commitments = []
# masks used in the output commitment
self.output_sk_masks = []
self.output_amounts = []
# output *range proof* masks
# output *range proof* masks. HP10+ makes them deterministic.
self.output_masks = []
# last output mask for client_version=0
self.output_last_mask = None
# the range proofs are calculated in batches, this denotes the grouping
self.rsig_grouping = []
@ -146,3 +149,9 @@ class State:
def change_address(self):
return self.output_change.addr if self.output_change else None
def is_bulletproof_v2(self):
return self.bp_version >= 2
def is_det_mask(self):
return self.bp_version >= 2 or self.client_version > 0

View File

@ -6,7 +6,6 @@ import gc
from apps.monero import misc, signing
from apps.monero.layout import confirms
from apps.monero.signing import RctType, RsigType
from apps.monero.signing.state import State
from apps.monero.xmr import crypto, monero
@ -28,6 +27,7 @@ async def init_transaction(
await paths.validate_path(state.ctx, misc.validate_full_path, path=address_n)
state.creds = misc.get_creds(keychain, address_n, network_type)
state.client_version = tsx_data.client_version or 0
state.fee = state.fee if state.fee > 0 else 0
state.tx_priv = crypto.random_scalar()
state.tx_pub = crypto.scalarmult_base(state.tx_priv)
@ -74,9 +74,9 @@ async def init_transaction(
state.mem_trace(10, True)
# Final message hasher
state.full_message_hasher.init(state.rct_type == RctType.Simple)
state.full_message_hasher.init()
state.full_message_hasher.set_type_fee(
signing.get_monero_rct_type(state.rct_type, state.rsig_type), state.fee
signing.get_monero_rct_type(state.bp_version), state.fee
)
# Sub address precomputation
@ -167,31 +167,32 @@ def _get_primary_change_address(state: State):
def _check_rsig_data(state: State, rsig_data: MoneroTransactionRsigData):
"""
There are two types of monero ring confidential transactions:
1. RCTTypeFull = 1 (used if num_inputs == 1)
2. RCTTypeSimple = 2 (for num_inputs > 1)
1. RCTTypeFull = 1 (used if num_inputs == 1 && Borromean)
2. RCTTypeSimple = 2 (for num_inputs > 1 || !Borromean)
and four types of range proofs (set in `rsig_data.rsig_type`):
1. RangeProofBorromean = 0
2. RangeProofBulletproof = 1
3. RangeProofMultiOutputBulletproof = 2
4. RangeProofPaddedBulletproof = 3
The current code supports only HF9, HF10 thus TX type is always simple
and RCT algorithm is always Bulletproof.
"""
state.rsig_grouping = rsig_data.grouping
if rsig_data.rsig_type == 0:
state.rsig_type = RsigType.Borromean
raise ValueError("Borromean range sig not supported")
elif rsig_data.rsig_type in (1, 2, 3):
state.rsig_type = RsigType.Bulletproof
state.bp_version = rsig_data.bp_version or 1
if state.bp_version not in (1, 2):
raise ValueError("Unknown BP version")
else:
raise ValueError("Unknown rsig type")
# unintuitively RctType.Simple is used for more inputs
if state.input_count > 1 or state.rsig_type == RsigType.Bulletproof:
state.rct_type = RctType.Simple
else:
state.rct_type = RctType.Full
if state.rsig_type == RsigType.Bulletproof and state.output_count > 2:
if state.output_count > 2:
state.rsig_offload = True
_check_grouping(state)
@ -293,18 +294,24 @@ def _process_payment_id(state: State, tsx_data: MoneroTransactionData):
therefore the TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID = 0x01 tag is used.
If it is not encrypted, we use TX_EXTRA_NONCE_PAYMENT_ID = 0x00.
Since Monero release 0.13 all 2 output payments have encrypted payment ID
to make BC more uniform.
See:
- https://github.com/monero-project/monero/blob/ff7dc087ae5f7de162131cea9dbcf8eac7c126a1/src/cryptonote_basic/tx_extra.h
"""
# encrypted payment id / dummy payment ID
view_key_pub_enc = None
if not tsx_data.payment_id or len(tsx_data.payment_id) == 8:
view_key_pub_enc = _get_key_for_payment_id_encryption(
tsx_data, state.change_address(), state.client_version > 0
)
if not tsx_data.payment_id:
return
# encrypted payment id
if len(tsx_data.payment_id) == 8:
view_key_pub_enc = _get_key_for_payment_id_encryption(
tsx_data.outputs, state.change_address()
)
elif len(tsx_data.payment_id) == 8:
view_key_pub = crypto.decodepoint(view_key_pub_enc)
payment_id_encr = _encrypt_payment_id(
tsx_data.payment_id, view_key_pub, state.tx_priv
@ -334,10 +341,15 @@ def _process_payment_id(state: State, tsx_data: MoneroTransactionData):
state.extra_nonce = extra_buff
def _get_key_for_payment_id_encryption(destinations: list, change_addr=None):
def _get_key_for_payment_id_encryption(
tsx_data: MoneroTransactionData,
change_addr=None,
add_dummy_payment_id: bool = False,
):
"""
Returns destination address public view key to be used for
payment id encryption.
payment id encryption. If no encrypted payment ID is chosen,
dummy payment ID is set for better transaction uniformity if possible.
"""
from apps.monero.xmr.addresses import addr_eq
from trezor.messages.MoneroAccountPublicAddress import MoneroAccountPublicAddress
@ -346,20 +358,24 @@ def _get_key_for_payment_id_encryption(destinations: list, change_addr=None):
spend_public_key=crypto.NULL_KEY_ENC, view_public_key=crypto.NULL_KEY_ENC
)
count = 0
for dest in destinations:
for dest in tsx_data.outputs:
if dest.amount == 0:
continue
if change_addr and addr_eq(dest.addr, change_addr):
continue
if addr_eq(dest.addr, addr):
continue
if count > 0:
if count > 0 and tsx_data.payment_id:
raise ValueError(
"Destinations have to have exactly one output to support encrypted payment ids"
)
addr = dest.addr
count += 1
# Insert dummy payment id for transaction uniformity
if not tsx_data.payment_id and count <= 1 and add_dummy_payment_id:
tsx_data.payment_id = bytearray(8)
if count == 0 and change_addr:
return change_addr.view_public_key
@ -380,6 +396,4 @@ def _encrypt_payment_id(payment_id, public_key, secret_key):
derivation[32] = 0x8D # ENCRYPTED_PAYMENT_ID_TAIL
hash = crypto.cn_fast_hash(derivation)
pm_copy = bytearray(payment_id)
for i in range(8):
pm_copy[i] ^= hash[i]
return pm_copy
return crypto.xor8(pm_copy, hash)

View File

@ -14,7 +14,6 @@ key derived for exactly this purpose.
from .state import State
from apps.monero.layout import confirms
from apps.monero.signing import RctType
from apps.monero.xmr import crypto, monero, serialize
if False:
@ -95,27 +94,19 @@ async def set_input(state: State, src_entr: MoneroTransactionSourceEntry):
state.mem_trace(3, True)
# PseudoOuts commitment, alphas stored to state
pseudo_out = None
pseudo_out_hmac = None
alpha_enc = None
alpha, pseudo_out = _gen_commitment(state, src_entr.amount)
pseudo_out = crypto.encodepoint(pseudo_out)
if state.rct_type == RctType.Simple:
alpha, pseudo_out = _gen_commitment(state, src_entr.amount)
pseudo_out = crypto.encodepoint(pseudo_out)
# In full version the alpha is encrypted and passed back for storage
pseudo_out_hmac = crypto.compute_hmac(
offloading_keys.hmac_key_txin_comm(state.key_hmac, state.current_input_index),
pseudo_out,
)
# In full version the alpha is encrypted and passed back for storage
pseudo_out_hmac = crypto.compute_hmac(
offloading_keys.hmac_key_txin_comm(
state.key_hmac, state.current_input_index
),
pseudo_out,
)
alpha_enc = chacha_poly.encrypt_pack(
offloading_keys.enc_key_txin_alpha(
state.key_enc, state.current_input_index
),
crypto.encodeint(alpha),
)
alpha_enc = chacha_poly.encrypt_pack(
offloading_keys.enc_key_txin_alpha(state.key_enc, state.current_input_index),
crypto.encodeint(alpha),
)
spend_enc = chacha_poly.encrypt_pack(
offloading_keys.enc_key_spend(state.key_enc, state.current_input_index),
@ -128,6 +119,7 @@ async def set_input(state: State, src_entr: MoneroTransactionSourceEntry):
the precomputed subaddresses so we clear them to save memory.
"""
state.subaddresses = None
state.input_last_amount = src_entr.amount
return MoneroTransactionSetInputAck(
vini=vini_bin,

View File

@ -7,7 +7,7 @@ Also hashes `pseudo_out` to the final_message.
from .state import State
from apps.monero.layout import confirms
from apps.monero.signing import RctType, RsigType, offloading_keys
from apps.monero.signing import offloading_keys
from apps.monero.xmr import crypto
if False:
@ -21,9 +21,18 @@ async def input_vini(
src_entr: MoneroTransactionSourceEntry,
vini_bin: bytes,
vini_hmac: bytes,
pseudo_out: bytes,
pseudo_out_hmac: bytes,
):
"""
This step serves for an incremental hashing of tx.vin[i] to the tx_prefix_hasher
after the sorting on tx.vin[i].ki.
Originally, this step also incrementaly hashed pseudo_output[i] to the full_message_hasher for
RctSimple transactions with Borromean proofs (HF8).
In later hard-forks, the pseudo_outputs were moved to the rctsig.prunable
which is not hashed to the final signature, thus pseudo_output hashing has been removed
(as we support only HF9 and HF10 now).
"""
from trezor.messages.MoneroTransactionInputViniAck import (
MoneroTransactionInputViniAck,
)
@ -50,24 +59,4 @@ async def input_vini(
Incremental hasing of tx.vin[i]
"""
state.tx_prefix_hasher.buffer(vini_bin)
# in monero version >= 8 pseudo outs were moved to a different place
# bulletproofs imply version >= 8
if state.rct_type == RctType.Simple and state.rsig_type != RsigType.Bulletproof:
_hash_vini_pseudo_out(state, pseudo_out, pseudo_out_hmac)
return MoneroTransactionInputViniAck()
def _hash_vini_pseudo_out(state: State, pseudo_out: bytes, pseudo_out_hmac: bytes):
"""
Incremental hasing of pseudo output. Only applicable for simple rct.
"""
idx = state.source_permutation[state.current_input_index]
pseudo_out_hmac_comp = crypto.compute_hmac(
offloading_keys.hmac_key_txin_comm(state.key_hmac, idx), pseudo_out
)
if not crypto.ct_equals(pseudo_out_hmac, pseudo_out_hmac_comp):
raise ValueError("HMAC invalid for pseudo outs")
state.full_message_hasher.set_pseudo_out(pseudo_out)

View File

@ -3,12 +3,9 @@ All inputs set. Defining range signature parameters.
If in the applicable offloading mode, generate commitment masks.
"""
from trezor import utils
from .state import State
from apps.monero.layout import confirms
from apps.monero.signing import RctType
from apps.monero.xmr import crypto
@ -20,39 +17,53 @@ async def all_inputs_set(state: State):
from trezor.messages.MoneroTransactionAllInputsSetAck import (
MoneroTransactionAllInputsSetAck,
)
from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData
# Generate random commitment masks to be used in range proofs.
# If SimpleRCT is used the sum of the masks must match the input masks sum.
state.sumout = crypto.sc_init(0)
for i in range(state.output_count):
cur_mask = crypto.new_scalar() # new mask for each output
is_last = i + 1 == state.output_count
if is_last and state.rct_type == RctType.Simple:
# in SimpleRCT the last mask needs to be calculated as an offset of the sum
crypto.sc_sub_into(cur_mask, state.sumpouts_alphas, state.sumout)
else:
crypto.random_scalar(cur_mask)
rsig_data = None
crypto.sc_add_into(state.sumout, state.sumout, cur_mask)
state.output_masks.append(cur_mask)
# Client 0, HF9. Non-deterministic masks
if not state.is_det_mask():
rsig_data = await _compute_masks(state)
if state.rct_type == RctType.Simple:
utils.ensure(
crypto.sc_eq(state.sumout, state.sumpouts_alphas), "Invalid masks sum"
) # sum check
state.sumout = crypto.sc_init(0)
resp = MoneroTransactionAllInputsSetAck(rsig_data=rsig_data)
return resp
async def _compute_masks(state: State):
"""
Output masks computed in advance. Used with client_version=0 && HF9.
After HF10 (included) masks are deterministic, computed from the amount_key.
After all client update to v1 this code will be removed.
In order to preserve client_version=0 compatibility the masks have to be adjusted.
"""
from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData
from apps.monero.signing import offloading_keys
rsig_data = MoneroTransactionRsigData()
resp = MoneroTransactionAllInputsSetAck(rsig_data=rsig_data)
# If range proofs are being offloaded, we send the masks to the host, which uses them
# to create the range proof. If not, we do not send any and we use them in the following step.
if state.rsig_offload:
tmp_buff = bytearray(32)
rsig_data.mask = bytearray(32 * state.output_count)
for i in range(state.output_count):
crypto.encodeint_into(tmp_buff, state.output_masks[i])
utils.memcpy(rsig_data.mask, 32 * i, tmp_buff, 0, 32)
rsig_data.mask = []
return resp
# Deterministic masks, the last one is computed to balance the sums
for i in range(state.output_count):
if i + 1 == state.output_count:
cur_mask = crypto.sc_sub(state.sumpouts_alphas, state.sumout)
state.output_last_mask = cur_mask
else:
cur_mask = offloading_keys.det_comm_masks(state.key_enc, i)
crypto.sc_add_into(state.sumout, state.sumout, cur_mask)
if state.rsig_offload:
rsig_data.mask.append(crypto.encodeint(cur_mask))
if not crypto.sc_eq(state.sumpouts_alphas, state.sumout):
raise ValueError("Sum eq error")
state.sumout = crypto.sc_init(0)
return rsig_data

View File

@ -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 intensive
rsig, mask = _range_proof(state, dst_entr.amount, rsig_data)
# Compute tx keys and masks if applicable
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"
)
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.current_input_index + 1 == state.input_count, "Invalid number of inputs"
)
utils.ensure(
state.current_output_index < state.output_count, "Invalid output index"
)
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)
if not state.rsig_offload:
"""Bulletproof calculation in Trezor"""
rsig = _rsig_bp(state)
# 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)
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
rsig = _dump_rsig_bp(rsig)
state.mem_trace(
"post-bp-ser, size: %s" % len(rsig) if __debug__ else None, collect=True
)
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 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

View File

@ -22,6 +22,7 @@ async def all_outputs_set(state: State):
state.mem_trace(1)
_validate(state)
state.is_processing_offloaded = False
state.mem_trace(2)
_set_tx_extra(state)
@ -51,7 +52,7 @@ async def all_outputs_set(state: State):
rv_pb = MoneroRingCtSig(
txn_fee=state.fee,
message=state.tx_prefix_hash,
rv_type=get_monero_rct_type(state.rct_type, state.rsig_type),
rv_type=get_monero_rct_type(state.bp_version),
)
_out_pk(state)
@ -71,15 +72,9 @@ async def all_outputs_set(state: State):
def _validate(state: State):
from apps.monero.signing import RctType
if state.current_output_index + 1 != state.output_count:
raise ValueError("Invalid out num")
# Test if \sum Alpha == \sum A
if state.rct_type == RctType.Simple:
utils.ensure(crypto.sc_eq(state.sumout, state.sumpouts_alphas))
# Fee test
if state.fee != (state.summary_inputs_money - state.summary_outs_money):
raise ValueError(

View File

@ -9,7 +9,6 @@ from trezor import utils
from .state import State
from apps.monero.layout import confirms
from apps.monero.signing import RctType
from apps.monero.xmr import crypto
if False:
@ -29,6 +28,16 @@ async def sign_input(
spend_enc: bytes,
):
"""
Signing UTXO.
Mask Balancing.
Sum of input masks has to be equal to the sum of output masks.
As the output masks has been made deterministic in HF10 the mask sum equality is corrected
in this step. The last input mask (and thus pseudo_out) is recomputed so the sums equal.
If deterministic masks cannot be used (client_version=0), the balancing is done in step 5
on output masks as pseudo outputs have to remain same.
:param state: transaction state
:param src_entr: Source entry
:param vini_bin: tx.vin[i] for the transaction. Contains key image, offsets, amount (usually zero)
@ -47,12 +56,10 @@ async def sign_input(
state.current_input_index += 1
if state.current_input_index >= state.input_count:
raise ValueError("Invalid inputs count")
if state.rct_type == RctType.Simple and pseudo_out is None:
if pseudo_out is None:
raise ValueError("SimpleRCT requires pseudo_out but none provided")
if state.rct_type == RctType.Simple and pseudo_out_alpha_enc is None:
if pseudo_out_alpha_enc is None:
raise ValueError("SimpleRCT requires pseudo_out's mask but none provided")
if state.current_input_index >= 1 and not state.rct_type == RctType.Simple:
raise ValueError("Two and more inputs must imply SimpleRCT")
input_position = state.source_permutation[state.current_input_index]
mods = utils.unimport_begin()
@ -71,7 +78,27 @@ async def sign_input(
from apps.monero.xmr.crypto import chacha_poly
if state.rct_type == RctType.Simple:
pseudo_out_alpha = crypto.decodeint(
chacha_poly.decrypt_pack(
offloading_keys.enc_key_txin_alpha(state.key_enc, input_position),
bytes(pseudo_out_alpha_enc),
)
)
# Last pseud_out is recomputed so mask sums hold
if state.is_det_mask() and input_position + 1 == state.input_count:
# Recompute the lash alpha so the sum holds
state.mem_trace("Correcting alpha")
alpha_diff = crypto.sc_sub(state.sumout, state.sumpouts_alphas)
crypto.sc_add_into(pseudo_out_alpha, pseudo_out_alpha, alpha_diff)
pseudo_out_c = crypto.gen_commitment(pseudo_out_alpha, state.input_last_amount)
else:
if input_position + 1 == state.input_count:
utils.ensure(
crypto.sc_eq(state.sumpouts_alphas, state.sumout), "Sum eq error"
)
# both pseudo_out and its mask were offloaded so we need to
# validate pseudo_out's HMAC and decrypt the alpha
pseudo_out_hmac_comp = crypto.compute_hmac(
@ -81,16 +108,10 @@ async def sign_input(
if not crypto.ct_equals(pseudo_out_hmac_comp, pseudo_out_hmac):
raise ValueError("HMAC is not correct")
state.mem_trace(2, True)
pseudo_out_alpha = crypto.decodeint(
chacha_poly.decrypt_pack(
offloading_keys.enc_key_txin_alpha(state.key_enc, input_position),
bytes(pseudo_out_alpha_enc),
)
)
pseudo_out_c = crypto.decodepoint(pseudo_out)
state.mem_trace(2, True)
# Spending secret
spend_key = crypto.decodeint(
chacha_poly.decrypt_pack(
@ -141,42 +162,24 @@ async def sign_input(
ring_pubkeys = [x.key for x in src_entr.outputs]
del src_entr
if state.rct_type == RctType.Simple:
mlsag.generate_mlsag_simple(
state.full_message,
ring_pubkeys,
input_secret_key,
pseudo_out_alpha,
pseudo_out_c,
kLRki,
index,
mg_buffer,
)
mlsag.generate_mlsag_simple(
state.full_message,
ring_pubkeys,
input_secret_key,
pseudo_out_alpha,
pseudo_out_c,
kLRki,
index,
mg_buffer,
)
del (input_secret_key, pseudo_out_alpha, pseudo_out_c)
else:
# Full RingCt, only one input
txn_fee_key = crypto.scalarmult_h(state.fee)
mlsag.generate_mlsag_full(
state.full_message,
ring_pubkeys,
input_secret_key,
state.output_sk_masks,
state.output_pk_commitments,
kLRki,
index,
txn_fee_key,
mg_buffer,
)
del (input_secret_key, txn_fee_key)
del (mlsag, ring_pubkeys)
del (input_secret_key, pseudo_out_alpha, mlsag, ring_pubkeys)
state.mem_trace(5, True)
from trezor.messages.MoneroTransactionSignInputAck import (
MoneroTransactionSignInputAck,
)
return MoneroTransactionSignInputAck(signature=mg_buffer)
return MoneroTransactionSignInputAck(
signature=mg_buffer, pseudo_out=crypto.encodepoint(pseudo_out_c)
)

View File

@ -25,8 +25,11 @@ keccak_hash = tcry.xmr_fast_hash
keccak_hash_into = tcry.xmr_fast_hash
def keccak_2hash(inp):
return keccak_hash(keccak_hash(inp))
def keccak_2hash(inp, buff=None):
buff = buff if buff else bytearray(32)
keccak_hash_into(buff, inp)
keccak_hash_into(buff, buff)
return buff
def compute_hmac(key, msg=None):
@ -168,7 +171,6 @@ https://www.imperialviolet.org/2013/12/25/elligator.html
http://elligator.cr.yp.to/
http://elligator.cr.yp.to/elligator-20130828.pdf
"""
ge_frombytes_vartime_check = tcry.ge25519_check
#
# Monero specific
@ -226,7 +228,7 @@ def generate_key_derivation(pub, sec):
Key derivation: 8*(key2*key1)
"""
sc_check(sec) # checks that the secret key is uniform enough...
ge_frombytes_vartime_check(pub)
check_ed25519point(pub)
return tcry.xmr_generate_key_derivation(pub, sec)
@ -242,9 +244,7 @@ def derive_public_key(derivation, output_index, B):
"""
H_s(derivation || varint(output_index))G + B
"""
ge_frombytes_vartime_check(B) # check some conditions on the point
check_ed25519point(B)
return tcry.xmr_derive_public_key(derivation, output_index, B)
@ -298,3 +298,9 @@ def check_signature(data, c, r, pub):
tmp_c = hash_to_scalar(buff)
res = sc_sub(tmp_c, c)
return not sc_isnonzero(res)
def xor8(buff, key):
for i in range(8):
buff[i] ^= key[i]
return buff

View File

@ -61,7 +61,7 @@ def generate_ring_signature(prefix_hash, image, pubs, sec, sec_idx, test=False):
if not crypto.point_eq(k_i, image):
raise ValueError("Key image invalid")
for k in pubs:
crypto.ge_frombytes_vartime_check(k)
crypto.check_ed25519point(k)
buff_off = len(prefix_hash)
buff = bytearray(buff_off + 2 * 32 * len(pubs))

View File

@ -49,68 +49,6 @@ from apps.monero.xmr import crypto
from apps.monero.xmr.serialize import int_serialize
def generate_mlsag_full(
message,
pubs,
in_sk,
out_sk_mask,
out_pk_commitments,
kLRki,
index,
txn_fee_key,
mg_buff,
):
cols = len(pubs)
if cols == 0:
raise ValueError("Empty pubs")
rows = 1 # Monero uses only one row
if len(out_sk_mask) != len(out_pk_commitments):
raise ValueError("Bad outsk/putpk size")
sk = _key_vector(rows + 1)
M = _key_matrix(rows + 1, cols)
tmp_mi_rows = crypto.new_point(None)
tmp_pt = crypto.new_point(None)
for i in range(cols):
crypto.identity_into(tmp_mi_rows) # M[i][rows]
# Should iterate over rows, simplified as rows == 1
M[i][0] = pubs[i].dest
crypto.point_add_into(
tmp_mi_rows,
tmp_mi_rows,
crypto.decodepoint_into(tmp_pt, pubs[i].commitment),
)
pubs[i] = None
for j in range(len(out_pk_commitments)):
crypto.point_sub_into(
tmp_mi_rows,
tmp_mi_rows,
crypto.decodepoint_into(tmp_pt, out_pk_commitments[j]),
) # subtract output Ci's in last row
# Subtract txn fee output in last row
crypto.point_sub_into(tmp_mi_rows, tmp_mi_rows, txn_fee_key)
M[i][rows] = crypto.encodepoint(tmp_mi_rows)
# Simplified as rows == 1
sk[0] = in_sk.dest
sk[rows] = in_sk.mask # originally: sum of all in_sk[0..rows] in sk[rows]
for j in range(len(out_pk_commitments)):
crypto.sc_sub_into(
sk[rows], sk[rows], out_sk_mask[j]
) # subtract output masks in last row
del (pubs, tmp_mi_rows, tmp_pt)
gc.collect()
return generate_mlsag(message, M, sk, kLRki, index, rows, mg_buff)
def generate_mlsag_simple(message, pubs, in_sk, a, cout, kLRki, index, mg_buff):
"""
MLSAG for RctType.Simple

View File

@ -8,18 +8,16 @@ class PreMlsagHasher:
"""
def __init__(self):
self.is_simple = None
self.state = 0
self.kc_master = crypto.get_keccak()
self.rsig_hasher = crypto.get_keccak()
self.rtcsig_hasher = KeccakXmrArchive()
def init(self, is_simple):
def init(self):
if self.state != 0:
raise ValueError("State error")
self.state = 1
self.is_simple = is_simple
def set_message(self, message):
self.kc_master.update(message)
@ -31,14 +29,6 @@ class PreMlsagHasher:
self.rtcsig_hasher.uint(rv_type, 1) # UInt8
self.rtcsig_hasher.uvarint(fee) # UVarintType
def set_pseudo_out(self, out):
if self.state != 2 and self.state != 3:
raise ValueError("State error")
self.state = 3
# Manual serialization of the ECKey
self.rtcsig_hasher.buffer(out)
def set_ecdh(self, ecdh):
if self.state != 2 and self.state != 3 and self.state != 4:
raise ValueError("State error")

View File

@ -259,3 +259,16 @@ def generate_sub_address_keys(view_sec, spend_pub, major, minor):
D = crypto.point_add(spend_pub, M)
C = crypto.scalarmult(D, view_sec)
return D, C
def commitment_mask(key, buff=None):
"""
Generates deterministic commitment mask for Bulletproof2
"""
data = bytearray(15 + 32)
data[0:15] = b"commitment_mask"
data[15:] = key
if buff:
return crypto.hash_to_scalar_into(buff, data)
else:
return crypto.hash_to_scalar(data)

View File

@ -1,7 +1,7 @@
"""
Computes range signature
Can compute Borromean range proof or Bulletproof.
Can compute Bulletproof. Borromean support was discontinued.
Also can verify Bulletproof, in case the computation was offloaded.
Mostly ported from official Monero client, but also inspired by Mininero.
@ -40,105 +40,3 @@ def verify_bp(bp_proof, amounts, masks):
res = bpi.verify(bp_proof)
gc.collect()
return res
def prove_range_borromean(amount, last_mask):
"""Calculates Borromean range proof"""
# The large chunks allocated first to avoid potential memory fragmentation issues.
ai = bytearray(32 * 64)
alphai = bytearray(32 * 64)
Cis = bytearray(32 * 64)
s0s = bytearray(32 * 64)
s1s = bytearray(32 * 64)
buff = bytearray(32)
ee_bin = bytearray(32)
a = crypto.sc_init(0)
si = crypto.sc_init(0)
c = crypto.sc_init(0)
ee = crypto.sc_init(0)
tmp_ai = crypto.sc_init(0)
tmp_alpha = crypto.sc_init(0)
C_acc = crypto.identity()
C_h = crypto.xmr_H()
C_tmp = crypto.identity()
L = crypto.identity()
kck = crypto.get_keccak()
for ii in range(64):
crypto.random_scalar(tmp_ai)
if last_mask is not None and ii == 63:
crypto.sc_sub_into(tmp_ai, last_mask, a)
crypto.sc_add_into(a, a, tmp_ai)
crypto.random_scalar(tmp_alpha)
crypto.scalarmult_base_into(L, tmp_alpha)
crypto.scalarmult_base_into(C_tmp, tmp_ai)
# if 0: C_tmp += Zero (nothing is added)
# if 1: C_tmp += 2^i*H
# 2^i*H is already stored in C_h
if (amount >> ii) & 1 == 1:
crypto.point_add_into(C_tmp, C_tmp, C_h)
crypto.point_add_into(C_acc, C_acc, C_tmp)
# Set Ci[ii] to sigs
crypto.encodepoint_into(Cis, C_tmp, ii << 5)
crypto.encodeint_into(ai, tmp_ai, ii << 5)
crypto.encodeint_into(alphai, tmp_alpha, ii << 5)
if ((amount >> ii) & 1) == 0:
crypto.random_scalar(si)
crypto.encodepoint_into(buff, L)
crypto.hash_to_scalar_into(c, buff)
crypto.point_sub_into(C_tmp, C_tmp, C_h)
crypto.add_keys2_into(L, si, c, C_tmp)
crypto.encodeint_into(s1s, si, ii << 5)
crypto.encodepoint_into(buff, L)
kck.update(buff)
crypto.point_double_into(C_h, C_h)
# Compute ee
tmp_ee = kck.digest()
crypto.decodeint_into(ee, tmp_ee)
del (tmp_ee, kck)
C_h = crypto.xmr_H()
gc.collect()
# Second pass, s0, s1
for ii in range(64):
crypto.decodeint_into(tmp_alpha, alphai, ii << 5)
crypto.decodeint_into(tmp_ai, ai, ii << 5)
if ((amount >> ii) & 1) == 0:
crypto.sc_mulsub_into(si, tmp_ai, ee, tmp_alpha)
crypto.encodeint_into(s0s, si, ii << 5)
else:
crypto.random_scalar(si)
crypto.encodeint_into(s0s, si, ii << 5)
crypto.decodepoint_into(C_tmp, Cis, ii << 5)
crypto.add_keys2_into(L, si, ee, C_tmp)
crypto.encodepoint_into(buff, L)
crypto.hash_to_scalar_into(c, buff)
crypto.sc_mulsub_into(si, tmp_ai, c, tmp_alpha)
crypto.encodeint_into(s1s, si, ii << 5)
crypto.point_double_into(C_h, C_h)
crypto.encodeint_into(ee_bin, ee)
del (ai, alphai, buff, tmp_ai, tmp_alpha, si, c, ee, C_tmp, C_h, L)
gc.collect()
return C_acc, a, [s0s, s1s, ee_bin, Cis]

View File

@ -28,6 +28,7 @@ class MoneroTransactionData(p.MessageType):
minor_indices: List[int] = None,
rsig_data: MoneroTransactionRsigData = None,
integrated_indices: List[int] = None,
client_version: int = None,
) -> None:
self.version = version
self.payment_id = payment_id
@ -41,6 +42,7 @@ class MoneroTransactionData(p.MessageType):
self.minor_indices = minor_indices if minor_indices is not None else []
self.rsig_data = rsig_data
self.integrated_indices = integrated_indices if integrated_indices is not None else []
self.client_version = client_version
@classmethod
def get_fields(cls):
@ -57,4 +59,5 @@ class MoneroTransactionData(p.MessageType):
10: ('minor_indices', p.UVarintType, p.FLAG_REPEATED),
11: ('rsig_data', MoneroTransactionRsigData, 0),
12: ('integrated_indices', p.UVarintType, p.FLAG_REPEATED),
13: ('client_version', p.UVarintType, 0),
}

View File

@ -12,10 +12,14 @@ class MoneroTransactionDestinationEntry(p.MessageType):
amount: int = None,
addr: MoneroAccountPublicAddress = None,
is_subaddress: bool = None,
original: bytes = None,
is_integrated: bool = None,
) -> None:
self.amount = amount
self.addr = addr
self.is_subaddress = is_subaddress
self.original = original
self.is_integrated = is_integrated
@classmethod
def get_fields(cls):
@ -23,4 +27,6 @@ class MoneroTransactionDestinationEntry(p.MessageType):
1: ('amount', p.UVarintType, 0),
2: ('addr', MoneroAccountPublicAddress, 0),
3: ('is_subaddress', p.BoolType, 0),
4: ('original', p.BytesType, 0),
5: ('is_integrated', p.BoolType, 0),
}

View File

@ -19,6 +19,7 @@ class MoneroTransactionRsigData(p.MessageType):
mask: bytes = None,
rsig: bytes = None,
rsig_parts: List[bytes] = None,
bp_version: int = None,
) -> None:
self.rsig_type = rsig_type
self.offload_type = offload_type
@ -26,6 +27,7 @@ class MoneroTransactionRsigData(p.MessageType):
self.mask = mask
self.rsig = rsig
self.rsig_parts = rsig_parts if rsig_parts is not None else []
self.bp_version = bp_version
@classmethod
def get_fields(cls):
@ -36,4 +38,5 @@ class MoneroTransactionRsigData(p.MessageType):
4: ('mask', p.BytesType, 0),
5: ('rsig', p.BytesType, 0),
6: ('rsig_parts', p.BytesType, p.FLAG_REPEATED),
7: ('bp_version', p.UVarintType, 0),
}

View File

@ -14,10 +14,12 @@ class MoneroTransactionSetOutputRequest(p.MessageType):
dst_entr: MoneroTransactionDestinationEntry = None,
dst_entr_hmac: bytes = None,
rsig_data: MoneroTransactionRsigData = None,
is_offloaded_bp: bool = None,
) -> None:
self.dst_entr = dst_entr
self.dst_entr_hmac = dst_entr_hmac
self.rsig_data = rsig_data
self.is_offloaded_bp = is_offloaded_bp
@classmethod
def get_fields(cls):
@ -25,4 +27,5 @@ class MoneroTransactionSetOutputRequest(p.MessageType):
1: ('dst_entr', MoneroTransactionDestinationEntry, 0),
2: ('dst_entr_hmac', p.BytesType, 0),
3: ('rsig_data', MoneroTransactionRsigData, 0),
4: ('is_offloaded_bp', p.BoolType, 0),
}

View File

@ -9,11 +9,14 @@ class MoneroTransactionSignInputAck(p.MessageType):
def __init__(
self,
signature: bytes = None,
pseudo_out: bytes = None,
) -> None:
self.signature = signature
self.pseudo_out = pseudo_out
@classmethod
def get_fields(cls):
return {
1: ('signature', p.BytesType, 0),
2: ('pseudo_out', p.BytesType, 0),
}

View File

@ -18,6 +18,9 @@ export EC_BACKEND_FORCE=1
export EC_BACKEND=1
export TREZOR_TEST_GET_TX=1
export TREZOR_TEST_LIVE_REFRESH=1
export TREZOR_TEST_SIGN_CL0_HF9=1
export TREZOR_TEST_SIGN_CL1_HF9=1
export TREZOR_TEST_SIGN_CL1_HF10=1
python3 -m unittest trezor_monero_test.test_trezor
error=$?
kill $upy_pid

@ -1 +1 @@
Subproject commit 0735c7d6f524b4c5108d201c789612aad7ce7920
Subproject commit cb238cb1f134accc4200217d9511115a8f61c6cb