Add offline subaddress generation

This commit is contained in:
Michał Sałaban 2019-01-03 01:32:56 +00:00
parent 6c1f667840
commit d0a2d35176
11 changed files with 409 additions and 32 deletions

View file

@ -1,4 +1,3 @@
import warnings
from . import prio
from .transaction import PaymentManager
@ -16,6 +15,7 @@ class Account(object):
:param index: the account's index within the wallet
"""
index = None
wallet = None
def __init__(self, backend, index):
self.index = index

View file

@ -0,0 +1,72 @@
from .. import exceptions
from ..account import Account
from ..address import Address
from ..seed import Seed
class WalletIsOffline(exceptions.BackendException):
pass
class OfflineWallet(object):
"""
Offline backend for Monero wallet. Provides support for address generation.
"""
_address = None
_svk = None
_ssk = None
def __init__(self, address, view_key=None, spend_key=None):
self._address = Address(address)
self._svk = view_key or self._svk
self._ssk = spend_key or self._ssk
def height(self):
raise WalletIsOffline()
def spend_key(self):
return self._ssk
def view_key(self):
return self._svk
def seed(self):
return Seed(self._ssk)
def accounts(self):
return [Account(self, 0)]
def new_account(self, label=None):
raise WalletIsOffline()
def addresses(self, account=0):
if account == 0:
return [self._address]
raise WalletIsOffline()
def new_address(self, account=0, label=None):
raise WalletIsOffline()
def balances(self, account=0):
raise WalletIsOffline()
def transfers_in(self, account, pmtfilter):
raise WalletIsOffline()
def transfers_out(self, account, pmtfilter):
raise WalletIsOffline()
def export_outputs(self):
raise WalletIsOffline()
def import_outputs(self, outputs_hex):
raise WalletIsOffline()
def export_key_images(self):
raise WalletIsOffline()
def import_key_images(self, key_images):
raise WalletIsOffline()
def transfer(self, *args, **kwargs):
raise WalletIsOffline()

View file

@ -49,6 +49,13 @@ def xrecover(y):
if x % 2 != 0: x = q-x
return x
def compress(P):
zinv = inv(P[2])
return (P[0] * zinv % q, P[1] * zinv % q)
def decompress(P):
return (P[0], P[1], 1, P[0]*P[1] % q)
By = 4 * inv(5)
Bx = xrecover(By)
B = [Bx%q, By%q]
@ -62,6 +69,20 @@ def edwards(P, Q):
y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2)
return [x3%q, y3%q]
def add(P, Q):
A = (P[1]-P[0])*(Q[1]-Q[0]) % q
B = (P[1]+P[0])*(Q[1]+Q[0]) % q
C = 2 * P[3] * Q[3] * d % q
D = 2 * P[2] * Q[2] % q
E = B-A
F = D-C
G = D+C
H = B+A
return (E*F, G*H, F*G, E*H)
def add_compressed(P, Q):
return compress(add(decompress(P), decompress(Q)))
def scalarmult(P, e):
if e == 0: return [0, 1]
Q = scalarmult(P, e//2)
@ -92,14 +113,6 @@ def Hint(m):
h = H(m)
return sum(2**i * bit(h, i) for i in range(2*b))
def signature(m, sk, pk):
h = H(sk)
a = 2**(b-2) + sum(2**i * bit(h, i) for i in range(3, b-2))
r = Hint(intlist2bytes([indexbytes(h, j) for j in range(b//8, b//4)]) + m)
R = scalarmult(B, r)
S = (r + Hint(encodepoint(R)+pk+m) * a) % l
return encodepoint(R) + encodeint(S)
def isoncurve(P):
x = P[0]
y = P[1]
@ -116,28 +129,29 @@ def decodepoint(s):
if not isoncurve(P): raise Exception("decoding point that is not on curve")
return P
def checkvalid(s, m, pk):
if len(s) != b//4: raise Exception("signature length is wrong")
if len(pk) != b//8: raise Exception("public-key length is wrong")
R = decodepoint(s[0:b//8])
A = decodepoint(pk)
S = decodeint(s[b//8:b//4])
h = Hint(encodepoint(R) + pk + m)
if scalarmult(B, S) != edwards(R, scalarmult(A, h)):
raise Exception("signature does not pass verification")
# This is from https://github.com/monero-project/mininero by Shen Noether with The Monero Project
def scalarmultbase(e):
if e == 0: return [0, 1]
Q = scalarmult(B, e//2)
Q = edwards(Q, Q)
if e & 1: Q = edwards(Q, B)
return Q
# These are unused but let's keep them
#def signature(m, sk, pk):
# h = H(sk)
# a = 2**(b-2) + sum(2**i * bit(h, i) for i in range(3, b-2))
# r = Hint(intlist2bytes([indexbytes(h, j) for j in range(b//8, b//4)]) + m)
# R = scalarmult(B, r)
# S = (r + Hint(encodepoint(R)+pk+m) * a) % l
# return encodepoint(R) + encodeint(S)
#
#def checkvalid(s, m, pk):
# if len(s) != b//4: raise Exception("signature length is wrong")
# if len(pk) != b//8: raise Exception("public-key length is wrong")
# R = decodepoint(s[0:b//8])
# A = decodepoint(pk)
# S = decodeint(s[b//8:b//4])
# h = Hint(encodepoint(R) + pk + m)
# if scalarmult(B, S) != edwards(R, scalarmult(A, h)):
# raise Exception("signature does not pass verification")
def public_from_secret(k):
keyInt = decodeint(k)
aG = scalarmultbase(keyInt)
return encodepoint(aG)
aB = scalarmult(B, keyInt)
return encodepoint(aB)
def public_from_secret_hex(hk):
return hexlify(public_from_secret(unhexlify(hk))).decode()

View file

@ -1,3 +1,10 @@
from binascii import hexlify, unhexlify
from sha3 import keccak_256
import struct
from . import address
from . import base58
from . import ed25519
from . import prio
from .transaction import Payment, PaymentManager
@ -36,6 +43,7 @@ class Wallet(object):
self.accounts = self.accounts or []
idx = 0
for _acc in self._backend.accounts():
_acc.wallet = self
try:
if self.accounts[idx]:
continue
@ -184,6 +192,38 @@ class Wallet(object):
"""
return self.accounts[0].new_address(label=label)
def get_address(self, major, minor):
"""
Calculates sub-address for account index (`major`) and address index within
the account (`minor`).
:rtype: :class:`BaseAddress <monero.address.BaseAddress>`
"""
master_address = self.address()
if major == minor == 0:
return master_address
master_svk = unhexlify(self.view_key())
master_psk = unhexlify(self.address().spend_key())
# m = Hs("SubAddr\0" || master_svk || major || minor)
hsdata = b''.join([
b'SubAddr\0', master_svk,
struct.pack('<I', major), struct.pack('<I', minor)])
m = keccak_256(hsdata).digest()
# TODO: OK, the hash is calculated correctly. What's missing here is ed25519 math
# to do the following:
# D = master_psk + m * B
D = ed25519.add_compressed(
ed25519.decodepoint(master_psk),
ed25519.scalarmult(ed25519.B, ed25519.decodeint(m)))
# C = master_svk * D
C = ed25519.scalarmult(D, ed25519.decodeint(master_svk))
netbyte = bytearray([
42 if master_address.is_mainnet() else \
63 if master_address.is_testnet() else 36])
data = netbyte + ed25519.encodepoint(D) + ed25519.encodepoint(C)
checksum = keccak_256(data).digest()[:4]
return address.SubAddress(base58.encode(hexlify(data + checksum)))
def transfer(self, address, amount,
priority=prio.NORMAL, payment_id=None, unlock_time=0,
relay=True):