Add first working version of output recognition; add tests

pull/93/head
Michał Sałaban 3 years ago
parent 1e98fe1cc0
commit ee03a86d8c

@ -125,6 +125,49 @@ class Transaction(object):
for outputs directed to the wallet, provided that matching subaddresses have been for outputs directed to the wallet, provided that matching subaddresses have been
already generated. already generated.
""" """
def _scan_pubkeys(svk, psk, stealth_address):
for keyidx, tx_key in enumerate(self.pubkeys):
hsdata = b"".join(
[
ed25519.encodepoint(
ed25519.scalarmult(
ed25519.decodepoint(tx_key), ed25519.decodeint(svk) * 8
)
),
varint.encode(idx),
]
)
Hs_ur = sha3.keccak_256(hsdata).digest()
# sc_reduce32:
Hsint_ur = ed25519.decodeint(Hs_ur)
Hsint = Hsint_ur % ed25519.l
Hs = ed25519.encodeint(Hsint)
k = ed25519.encodepoint(
ed25519.edwards_add(
ed25519.scalarmult_B(Hsint), ed25519.decodepoint(psk),
)
)
if k != stealth_address:
continue
if not encamount:
# Tx ver 1
break
amount_hs = sha3.keccak_256(b"amount" + Hs).digest()
xormask = amount_hs[:len(encamount)]
dec_amount = bytes(a ^ b for a, b in zip(encamount, xormask))
int_amount = int.from_bytes(
dec_amount, byteorder="little", signed=False
)
amount = from_atomic(int_amount)
return Payment(
amount=amount,
timestamp=self.timestamp,
transaction=self,
local_address=addr)
if not self.json: if not self.json:
raise exceptions.TransactionWithoutJSON( raise exceptions.TransactionWithoutJSON(
'Tx {:s} has no .json attribute'.format(self.hash)) 'Tx {:s} has no .json attribute'.format(self.hash))
@ -133,66 +176,32 @@ class Transaction(object):
ep = ExtraParser(self.json["extra"]) ep = ExtraParser(self.json["extra"])
extra = ep.parse() extra = ep.parse()
self.pubkeys = extra.get("pubkeys", []) self.pubkeys = extra.get("pubkeys", [])
svk = wallet.view_key() svk = binascii.unhexlify(wallet.view_key())
# fetch before loop to save on calls; cast to list to preserve over multiple iterations
addresses = list(
itertools.chain(*map(operator.methodcaller("addresses"), wallet.accounts)))
outs = [] outs = []
for idx, vout in enumerate(self.json['vout']): for idx, vout in enumerate(self.json['vout']):
try: stealth_address = binascii.unhexlify(vout['target']['key'])
extra_pubkey = self.pubkeys[idx]
except IndexError:
extra_pubkey = None
if self.version == 2 and not self.is_coinbase: if self.version == 2 and not self.is_coinbase:
encamount = binascii.unhexlify( encamount = binascii.unhexlify(
self.json["rct_signatures"]["ecdhInfo"][idx]["amount"] self.json["rct_signatures"]["ecdhInfo"][idx]["amount"]
) )
payment = None
amount = from_atomic(vout['amount']) amount = from_atomic(vout['amount'])
if wallet: if wallet:
for addr in itertools.chain(map( for addridx, addr in enumerate(addresses):
operator.methodcaller('addresses', wallet.accounts))): psk = binascii.unhexlify(addr.spend_key())
psk = addr.spend_key() payment = _scan_pubkeys(svk, psk, stealth_address)
svk = binascii.unhexlify(addr.wallet.svk) if payment:
for keyidx, tx_key in enumerate(self.pubkeys):
hsdata = b"".join(
[
ed25519.encodepoint(
ed25519.scalarmult(
ed25519.decodepoint(tx_key), ed25519.decodeint(svk) * 8
)
),
varint.encode(idx),
]
)
Hs_ur = sha3.keccak_256(hsdata).digest()
# sc_reduce32:
Hsint_ur = ed25519.decodeint(Hs_ur)
Hsint = Hsint_ur % ed25519.l
Hs = ed25519.encodeint(Hsint)
k = ed25519.encodepoint(
ed25519.edwards_add(
ed25519.scalarmult_B(Hsint), ed25519.decodepoint(psk),
)
)
if k != vout['target']['key']:
continue
if not encamount:
# Tx ver 1
break
amount_hs = sha3.keccak_256(b"amount" + Hs).digest()
xormask = amount_hs[:len(encamount)]
dec_amount = bytes(a ^ b for a, b in zip(encamount, xormask))
int_amount = int.from_bytes(
dec_amount, byteorder="little", signed=False
)
amount = from_atomic(int_amount)
break break
outs.append(OneTimeOutput( outs.append(OneTimeOutput(
pubkey=vout['target']['key'], stealth_address=vout['target']['key'],
extra_pubkey=extra_pubkey, amount=payment.amount if payment else amount,
amount=amount,
index=self.output_indices[idx] if self.output_indices else None, index=self.output_indices[idx] if self.output_indices else None,
height=self.height, height=self.height,
transaction=self)) transaction=self,
payment=payment))
return outs return outs
def __repr__(self): def __repr__(self):
@ -201,43 +210,44 @@ class Transaction(object):
class OneTimeOutput(object): class OneTimeOutput(object):
""" """
A Monero one-time public output (A.K.A stealth address). Identified by `pubkey`, or `index` and `amount` A Monero one-time public output (A.K.A stealth address).
Identified by `stealth_address`, or `index` and `amount`
together, it can contain differing levels of information on an output. together, it can contain differing levels of information on an output.
This class is not intended to be turned into objects by the user, This class is not intended to be turned into objects by the user,
it is used by backends. it is used by backends.
""" """
pubkey = None stealth_address = None
extra_pubkey = None
amount = None amount = None
index = None index = None
height = None height = None
mask = None mask = None
transaction = None transaction = None
payment = None
unlocked = None unlocked = None
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.pubkey = kwargs.get('pubkey', self.pubkey) self.stealth_address = kwargs.get('stealth_address', self.stealth_address)
self.extra_pubkey = kwargs.get('extra_pubkey', self.extra_pubkey)
self.amount = kwargs.get('amount', self.amount) self.amount = kwargs.get('amount', self.amount)
self.index = kwargs.get('index', self.index) self.index = kwargs.get('index', self.index)
self.height = kwargs.get('height', self.height) self.height = kwargs.get('height', self.height)
self.mask = kwargs.get('mask', self.mask) self.mask = kwargs.get('mask', self.mask)
self.transaction = kwargs.get('transaction', self.transaction) self.transaction = kwargs.get('transaction', self.transaction)
self.payment = kwargs.get('payment', self.payment)
self.unlocked = kwargs.get('unlocked', self.unlocked) self.unlocked = kwargs.get('unlocked', self.unlocked)
def __repr__(self): def __repr__(self):
# Try to represent output as (index, amount) pair if applicable because there is no RPC # Try to represent output as (index, amount) pair if applicable because there is no RPC
# daemon command to lookup outputs by their pubkey ;( # daemon command to lookup outputs by their stealth_address ;(
if self.pubkey: if self.stealth_address:
return self.pubkey return self.stealth_address
else: else:
return '(index={},amount={})'.format(self.index, self.amount) return '(index={},amount={})'.format(self.index, self.amount)
def __eq__(self, other): def __eq__(self, other):
# Try to compare pubkeys, then try to compare (index,amount) pairs, else raise error # Try to compare stealth_addresses, then try to compare (index,amount) pairs, else raise error
if self.pubkey and other.pubkey: if self.stealth_address and other.stealth_address:
return self.pubkey == other.pubkey return self.stealth_address == other.stealth_address
elif None not in (self.index, other.index, self.amount, other.amount): elif None not in (self.index, other.index, self.amount, other.amount):
return self.index == other.index and self.amount == other.amount return self.index == other.index and self.amount == other.amount
else: else:

File diff suppressed because one or more lines are too long

@ -0,0 +1,26 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"subaddress_accounts": [
{
"account_index": 0,
"balance": 13387591827179,
"base_address": "56eDKfprZtQGfB4y6gVLZx5naKVHw6KEKLDoq2WWtLng9ANuBvsw67wfqyhQECoLmjQN4cKAdvMp2WsC5fnw9seKLcCSfjj",
"label": "Primary account",
"tag": "",
"unlocked_balance": 13387591827179
},
{
"account_index": 1,
"balance": 221310000000,
"base_address": "7BC3q5ogPCfTkBHZajDdkhSLxN3wSSULEN52Q2XzGebeetyG4oumiCHJjPpSyNvP6qR2idCYiUEqmHjKwc66fmcKN4dxW5u",
"label": "(Untitled account)",
"tag": "",
"unlocked_balance": 221310000000
}
],
"total_balance": 13608901827179,
"total_unlocked_balance": 13608901827179
}
}

@ -0,0 +1,7 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"key": "e507923516f52389eae889b6edc182ada82bb9354fb405abedbe0772a15aea0a"
}
}

@ -0,0 +1,15 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"address": "7BC3q5ogPCfTkBHZajDdkhSLxN3wSSULEN52Q2XzGebeetyG4oumiCHJjPpSyNvP6qR2idCYiUEqmHjKwc66fmcKN4dxW5u",
"addresses": [
{
"address": "7BC3q5ogPCfTkBHZajDdkhSLxN3wSSULEN52Q2XzGebeetyG4oumiCHJjPpSyNvP6qR2idCYiUEqmHjKwc66fmcKN4dxW5u",
"address_index": 0,
"label": "(Untitled account)",
"used": true
}
]
}
}

@ -0,0 +1,81 @@
from decimal import Decimal
try:
from unittest.mock import patch, Mock
except ImportError:
from mock import patch, Mock
import responses
from monero.daemon import Daemon
from monero.wallet import Wallet
from monero.backends.jsonrpc import JSONRPCDaemon, JSONRPCWallet
from .base import JSONTestCase
class OutputTestCase(JSONTestCase):
data_subdir = "test_outputs"
daemon_transactions_url = "http://127.0.0.1:38081/get_transactions"
wallet_jsonrpc_url = "http://127.0.0.1:38083/json_rpc"
@responses.activate
def test_multiple_outputs(self):
daemon = Daemon(JSONRPCDaemon(host="127.0.0.1", port=38081))
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-00-get_accounts.json"),
status=200)
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-01-query_key.json"),
status=200)
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-02-addresses-account-0.json"),
status=200)
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-02-addresses-account-1.json"),
status=200)
wallet = Wallet(JSONRPCWallet(host="127.0.0.1", port=38083))
responses.add(responses.POST, self.daemon_transactions_url,
json=self._read("test_multiple_outputs-daemon-00-get_transactions.json"),
status=200)
tx = daemon.transactions(
"f79a10256859058b3961254a35a97a3d4d5d40e080c6275a3f9779acde73ca8d")[0]
outs = tx.outputs(wallet=wallet)
self.assertEqual(len(outs), 5)
self.assertEqual(
outs[0].stealth_address,
"d3eb42322566c1d48685ee0d1ad7aed2ba6210291a785ec051d8b13ae797d202")
self.assertEqual(
outs[1].stealth_address,
"5bda44d7953e27b84022399850b59ed87408facdf00bbd1a2d4fda4bf9ebf72f")
self.assertEqual(
outs[2].stealth_address,
"4c79c14d5d78696e72959a28a734ec192059ebabb931040b5a0714c67b507e76")
self.assertEqual(
outs[3].stealth_address,
"64de2b358cdf96d498a9688edafcc0e25c60179e813304747524c876655a8e55")
self.assertEqual(
outs[4].stealth_address,
"966240954892294091a48c599c6db2b028e265c67677ed113d2263a7538f9a43")
self.assertIsNotNone(outs[0].payment)
self.assertIsNone(outs[1].payment) # FIXME: isn't that change we should recognize?
self.assertIsNotNone(outs[2].payment)
self.assertIsNotNone(outs[3].payment)
self.assertIsNotNone(outs[4].payment)
self.assertEqual(outs[0].amount, outs[0].payment.amount)
self.assertEqual(outs[2].amount, outs[2].payment.amount)
self.assertEqual(outs[3].amount, outs[3].payment.amount)
self.assertEqual(outs[4].amount, outs[4].payment.amount)
self.assertEqual(outs[0].amount, Decimal(4))
self.assertEqual(outs[2].amount, Decimal(1))
self.assertEqual(outs[3].amount, Decimal(2))
self.assertEqual(outs[4].amount, Decimal(8))
self.assertEqual(
outs[0].payment.local_address,
"76Qt2xMZ3m7b2tagubEgkvG81pwf9P3JYdxR65H2BEv8c79A9pCBTacEFv87tfdcqXRemBsZLFVGHTWbqBpkoBJENBoJJS9")
self.assertEqual(
outs[2].payment.local_address,
"78zGgzb45TEL8uvRFjCayUjHS98RFry1f7P4PE4LU7oeLh42s9AtP8fYXVzWqUW4r3Nz4g3V64w9RSiV7o3zUbPZVs5DVaU")
self.assertEqual(
outs[3].payment.local_address,
"73ndji4W2bu4WED87rJDVALMvUsZLLYstZsigbcGfb5YG9SuNyCSYk7Qbttez2mXciKtWRzRN9aYGJbF9TPBidNQNZppnFw")
self.assertEqual(
outs[4].payment.local_address,
"7BJxHKTa4p5USJ9Z5GY15ZARXL6Qe84qT3FnWkMbSJSoEj9ugGjnpQ1N9H1jqkjsTzLiN5VTbCP8f4MYYVPAcXhr36bHXzP")

@ -52,7 +52,7 @@ class FiltersTestCase(unittest.TestCase):
self.assertEqual(out1.transaction, self.tx2) self.assertEqual(out1.transaction, self.tx2)
self.assertEqual(out2.transaction, self.tx2) self.assertEqual(out2.transaction, self.tx2)
self.assertIn(self.json1['vout'][0]['target']['key'], repr(out1)) self.assertIn(self.json1['vout'][0]['target']['key'], repr(out1))
self.assertFalse(out2 != OneTimeOutput(pubkey=self.json1['vout'][1]['target']['key'])) self.assertFalse(out2 != OneTimeOutput(stealth_address=self.json1['vout'][1]['target']['key']))
self.assertIn('(index=25973289,amount=0E-12)', repr(self.oto1)) self.assertIn('(index=25973289,amount=0E-12)', repr(self.oto1))
self.assertEqual(self.oto1, OneTimeOutput(index=25973289, amount=Decimal('0.000000000000'))) self.assertEqual(self.oto1, OneTimeOutput(index=25973289, amount=Decimal('0.000000000000')))

Loading…
Cancel
Save