Skip to content

Commit

Permalink
xmr: mlsag notes
Browse files Browse the repository at this point in the history
  • Loading branch information
tsusanka committed Oct 17, 2018
1 parent c27ae90 commit 3fb57da
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 53 deletions.
17 changes: 7 additions & 10 deletions src/apps/monero/protocol/signing/step_09_sign_input.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Generates a signature for one input.
Generates a MLSAG signature for one input.
"""

import gc
Expand Down Expand Up @@ -128,15 +128,13 @@ async def sign_input(
gc.collect()
state.mem_trace(4)

# RCT signature
from apps.monero.xmr import mlsag

if state.rct_type == RctType.Simple:
# Simple RingCT
mix_ring = [x.key for x in src_entr.outputs]
mg, msc = mlsag.prove_rct_mg_simple(
ring_pubkeys = [x.key for x in src_entr.outputs]
mg = mlsag.generate_mlsag_simple(
state.full_message,
mix_ring,
ring_pubkeys,
input_secret_key,
pseudo_out_alpha,
pseudo_out_c,
Expand All @@ -147,11 +145,10 @@ async def sign_input(
else:
# Full RingCt, only one input
txn_fee_key = crypto.scalarmult_h(state.fee)
mix_ring = [[x.key] for x in src_entr.outputs]

mg, msc = mlsag.prove_rct_mg(
ring_pubkeys = [[x.key] for x in src_entr.outputs]
mg = mlsag.generate_mlsag_full(
state.full_message,
mix_ring,
ring_pubkeys,
[input_secret_key],
state.output_sk_masks,
state.output_pk_commitments,
Expand Down
145 changes: 102 additions & 43 deletions src/apps/monero/xmr/mlsag.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
# Author: https://github.com/monero-project/mininero
# Author: Dusan Klinec, ph4r05, 2018
# see https://eprint.iacr.org/2015/1098.pdf
"""
Multilayer Linkable Spontaneous Anonymous Group (MLSAG)
Optimized versions with incremental hashing.
Both Simple and Full Monero tx types are supported.
See https://eprint.iacr.org/2015/1098.pdf for details.
Also explained in From Zero to Monero section 3.3 and 5.
----------
Please note, that the MLSAG code is written in a generic manner,
where it is designed for multiple public keys (aka inputs). In another
words, MLSAG should be used to sign multiple inputs, but that is currently
not the case of Monero, where the inputs are signed one by one.
So the public keys matrix has always two rows (one for public keys,
one for commitments), although the algorithm is designed for `n` rows.
This has one unfortunate effect where `rows` is always equal to 2 and
dsRows always to 1, but the algorithm is still written as the numbers
can be arbitrary. That's why there are loops such as `for i in range(dsRows)`
where it is run only once currently.
----------
Also note, that the matrix of public keys is indexed by columns first.
This is because the code was ported from the official Monero client,
which is written in C++ and where it does have some memory advantages.
For ring size = 3 and one input the matrix M will look like this:
|------------------------|------------------------|------------------------|
| public key 0 | public key 1 | public key 2 |
| cmt 0 - pseudo_out cmt | cmt 1 - pseudo_out cmt | cmt 2 - pseudo_out cmt |
and `sk` is equal to:
|--------------|-----------------------------------------------------|
| private key* | input secret key's mask - pseudo_out's mask (alpha) |
* corresponding to one of the public keys (`index` denotes which one)
----------
Mostly ported from official Monero client, but also inspired by Mininero.
Author: Dusan Klinec, ph4r05, 2018
"""
from apps.monero.xmr import crypto


Expand All @@ -19,19 +59,13 @@ def key_matrix(rows, cols):
return rv


def scalar_gen_vector(n):
def _generate_random_vector(n):
"""
Generates vector of scalars
Generates vector of random scalars
"""
return [crypto.random_scalar() for _ in range(0, n)]


#
# Optimized versions with incremental hashing,
# Simple and full variants for Monero
#


def hasher_message(message):
"""
Returns incremental hasher for MLSAG
Expand All @@ -48,7 +82,7 @@ def hash_point(hasher, point, tmp_buff):

def gen_mlsag_assert(pk, xx, kLRki, index, dsRows):
"""
Conditions check for gen_mlsag_ext.
Conditions check
"""
cols = len(pk)
if cols <= 1:
Expand All @@ -74,9 +108,18 @@ def gen_mlsag_assert(pk, xx, kLRki, index, dsRows):
return rows, cols


def gen_mlsag_rows(message, rv, pk, xx, kLRki, index, dsRows, rows, cols):
def generate_first_c_and_key_images(message, rv, pk, xx, kLRki, index, dsRows, rows, cols):
"""
MLSAG computation - the part with secret keys
:param message: the full message to be signed (actually its hash)
:param rv: MgSig
:param pk: matrix of public keys and commitments
:param xx: input secret array composed of a private key and commitment mask
:param kLRki: used only in multisig, currently not implemented
:param index: specifies corresponding public key to the `xx`'s private key in the `pk` array
:param dsRows: row number where the pubkeys "end" (and commitments follow)
:param rows: total number of rows
:param cols: size of ring
"""
Ip = key_vector(dsRows)
rv.II = key_vector(dsRows)
Expand All @@ -87,6 +130,8 @@ def gen_mlsag_rows(message, rv, pk, xx, kLRki, index, dsRows, rows, cols):
hasher = hasher_message(message)

for i in range(dsRows):
# this is somewhat extra as compared to the Ring Confidential Tx paper
# see footnote in From Zero to Monero section 3.3
hasher.update(crypto.encodepoint(pk[index][i]))
if kLRki:
raise NotImplementedError("Multisig not implemented")
Expand All @@ -98,8 +143,11 @@ def gen_mlsag_rows(message, rv, pk, xx, kLRki, index, dsRows, rows, cols):
else:
Hi = crypto.hash_to_point(crypto.encodepoint(pk[index][i]))
alpha[i] = crypto.random_scalar()
# L = alpha_i * G
aGi = crypto.scalarmult_base(alpha[i])
# Ri = alpha_i * H(P_i)
aHPi = crypto.scalarmult(Hi, alpha[i])
# key image
rv.II[i] = crypto.scalarmult(Hi, xx[i])
hash_point(hasher, aGi, tmp_buff)
hash_point(hasher, aHPi, tmp_buff)
Expand All @@ -108,18 +156,31 @@ def gen_mlsag_rows(message, rv, pk, xx, kLRki, index, dsRows, rows, cols):

for i in range(dsRows, rows):
alpha[i] = crypto.random_scalar()
# L = alpha_i * G
aGi = crypto.scalarmult_base(alpha[i])
# for some reasons we omit calculating R here, which seems
# contrary to the paper, but it is in the Monero official client
# see https://github.com/monero-project/monero/blob/636153b2050aa0642ba86842c69ac55a5d81618d/src/ringct/rctSigs.cpp#L191
hash_point(hasher, pk[index][i], tmp_buff)
hash_point(hasher, aGi, tmp_buff)

# the first c
c_old = hasher.digest()
c_old = crypto.decodeint(c_old)
return c_old, Ip, alpha


def gen_mlsag_ext(message, pk, xx, kLRki, index, dsRows):
def generate_mlsag(message, pk, xx, kLRki, index, dsRows):
"""
Multilayered Spontaneous Anonymous Group Signatures (MLSAG signatures)
:param message: the full message to be signed (actually its hash)
:param pk: matrix of public keys and commitments
:param xx: input secret array composed of a private key and commitment mask
:param kLRki: used only in multisig, currently not implemented
:param index: specifies corresponding public key to the `xx`'s private key in the `pk` array
:param dsRows: separates pubkeys from commitment
:return MgSig
"""
from apps.monero.xmr.serialize_messages.tx_full import MgSig

Expand All @@ -128,7 +189,8 @@ def gen_mlsag_ext(message, pk, xx, kLRki, index, dsRows):
rv = MgSig()
c, L, R, Hi = 0, None, None, None

c_old, Ip, alpha = gen_mlsag_rows(
# calculates the "first" c, key images and random scalars alpha
c_old, Ip, alpha = generate_first_c_and_key_images(
message, rv, pk, xx, kLRki, index, dsRows, rows, cols
)

Expand All @@ -138,18 +200,21 @@ def gen_mlsag_ext(message, pk, xx, kLRki, index, dsRows):

tmp_buff = bytearray(32)
while i != index:
rv.ss[i] = scalar_gen_vector(rows)
rv.ss[i] = _generate_random_vector(rows)
hasher = hasher_message(message)

for j in range(dsRows):
# L = rv.ss[i][j] * G + c_old * pk[i][j]
L = crypto.add_keys2(rv.ss[i][j], c_old, pk[i][j])
Hi = crypto.hash_to_point(crypto.encodepoint(pk[i][j]))
R = crypto.add_keys3(rv.ss[i][j], Hi, c_old, Ip[j])
# R = rv.ss[i][j] * H(pk[i][j]) + c_old * Ip[j]
R = crypto.add_keys3(rv.ss[i][j], Hi, c_old, rv.II[j])
hash_point(hasher, pk[i][j], tmp_buff)
hash_point(hasher, L, tmp_buff)
hash_point(hasher, R, tmp_buff)

for j in range(dsRows, rows):
# again, omitting R here as discussed above
L = crypto.add_keys2(rv.ss[i][j], c_old, pk[i][j])
hash_point(hasher, pk[i][j], tmp_buff)
hash_point(hasher, L, tmp_buff)
Expand All @@ -164,18 +229,12 @@ def gen_mlsag_ext(message, pk, xx, kLRki, index, dsRows):
for j in range(rows):
rv.ss[index][j] = crypto.sc_mulsub(c, xx[j], alpha[j])

return rv, c
return rv


def prove_rct_mg(
def generate_mlsag_full(
message, pubs, in_sk, out_sk_mask, out_pk_commitments, kLRki, index, txn_fee_key
):
"""
c.f. http://eprint.iacr.org/2015/1098 section 4. definition 10.
This does the MG sig on the "dest" part of the given key matrix, and
the last row is the sum of input commitments from that column - sum output commitments
this shows that sum inputs = sum outputs
"""
cols = len(pubs)
if cols == 0:
raise ValueError("Empty pubs")
Expand Down Expand Up @@ -223,31 +282,31 @@ def prove_rct_mg(
sk[rows], out_sk_mask[j]
) # subtract output masks in last row

return gen_mlsag_ext(message, M, sk, kLRki, index, rows)
return generate_mlsag(message, M, sk, kLRki, index, rows)


def prove_rct_mg_simple(message, pubs, in_sk, a, cout, kLRki, index):
def generate_mlsag_simple(message, pubs, in_sk, a, cout, kLRki, index):
"""
Simple version for when we assume only
post rct inputs
here pubs is a vector of (P, C) length mixin
:param message:
:param pubs: vector of CtKeys, public, point values, encoded form. (dest, mask) = (P, C)
:param in_sk: CtKey, private. (spending private key, input commitment mask (original))
:param a: mask from the pseudo_output commitment (alpha)
:param cout: point, decoded. Pseudo output public key.
:param kLRki:
:param index:
:return:
MLSAG for RctType.Simple
:param message: the full message to be signed (actually its hash)
:param pubs: vector of MoneroRctKey; this forms the ring; point values in encoded form; (dest, mask) = (P, C)
:param in_sk: CtKey; spending private key with input commitment mask (original); better_name: input_secret_key
:param a: mask from the pseudo output commitment; better name: pseudo_out_alpha
:param cout: pseudo output commitment; point, decoded; better name: pseudo_out_c
:param kLRki: used only in multisig, currently not implemented
:param index: specifies corresponding public key to the `in_sk` in the pubs array
:return: MgSig
"""
rows = 1
# Monero signs inputs separately, so `rows` always equals 2 (pubkey, commitment)
# and `dsRows` is always 1 (denotes where the pubkeys "end")
rows = 2
dsRows = 1
cols = len(pubs)
if cols == 0:
raise ValueError("Empty pubs")

sk = key_vector(rows + 1)
M = key_matrix(rows + 1, cols)
sk = key_vector(rows)
M = key_matrix(rows, cols)

sk[0] = in_sk.dest
sk[1] = crypto.sc_sub(in_sk.mask, a)
Expand All @@ -256,4 +315,4 @@ def prove_rct_mg_simple(message, pubs, in_sk, a, cout, kLRki, index):
M[i][0] = crypto.decodepoint(pubs[i].dest)
M[i][1] = crypto.point_sub(crypto.decodepoint(pubs[i].mask), cout)

return gen_mlsag_ext(message, M, sk, kLRki, index, rows)
return generate_mlsag(message, M, sk, kLRki, index, dsRows)

0 comments on commit 3fb57da

Please sign in to comment.