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
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:
raise exceptions.TransactionWithoutJSON(
'Tx {:s} has no .json attribute'.format(self.hash))
@ -133,66 +176,32 @@ class Transaction(object):
ep = ExtraParser(self.json["extra"])
extra = ep.parse()
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 = []
for idx, vout in enumerate(self.json['vout']):
try:
extra_pubkey = self.pubkeys[idx]
except IndexError:
extra_pubkey = None
stealth_address = binascii.unhexlify(vout['target']['key'])
if self.version == 2 and not self.is_coinbase:
encamount = binascii.unhexlify(
self.json["rct_signatures"]["ecdhInfo"][idx]["amount"]
)
payment = None
amount = from_atomic(vout['amount'])
if wallet:
for addr in itertools.chain(map(
operator.methodcaller('addresses', wallet.accounts))):
psk = addr.spend_key()
svk = binascii.unhexlify(addr.wallet.svk)
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)
for addridx, addr in enumerate(addresses):
psk = binascii.unhexlify(addr.spend_key())
payment = _scan_pubkeys(svk, psk, stealth_address)
if payment:
break
outs.append(OneTimeOutput(
pubkey=vout['target']['key'],
extra_pubkey=extra_pubkey,
amount=amount,
stealth_address=vout['target']['key'],
amount=payment.amount if payment else amount,
index=self.output_indices[idx] if self.output_indices else None,
height=self.height,
transaction=self))
transaction=self,
payment=payment))
return outs
def __repr__(self):
@ -201,43 +210,44 @@ class Transaction(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.
This class is not intended to be turned into objects by the user,
it is used by backends.
"""
pubkey = None
extra_pubkey = None
stealth_address = None
amount = None
index = None
height = None
mask = None
transaction = None
payment = None
unlocked = None
def __init__(self, **kwargs):
self.pubkey = kwargs.get('pubkey', self.pubkey)
self.extra_pubkey = kwargs.get('extra_pubkey', self.extra_pubkey)
self.stealth_address = kwargs.get('stealth_address', self.stealth_address)
self.amount = kwargs.get('amount', self.amount)
self.index = kwargs.get('index', self.index)
self.height = kwargs.get('height', self.height)
self.mask = kwargs.get('mask', self.mask)
self.transaction = kwargs.get('transaction', self.transaction)
self.payment = kwargs.get('payment', self.payment)
self.unlocked = kwargs.get('unlocked', self.unlocked)
def __repr__(self):
# Try to represent output as (index, amount) pair if applicable because there is no RPC
# daemon command to lookup outputs by their pubkey ;(
if self.pubkey:
return self.pubkey
# daemon command to lookup outputs by their stealth_address ;(
if self.stealth_address:
return self.stealth_address
else:
return '(index={},amount={})'.format(self.index, self.amount)
def __eq__(self, other):
# Try to compare pubkeys, then try to compare (index,amount) pairs, else raise error
if self.pubkey and other.pubkey:
return self.pubkey == other.pubkey
# Try to compare stealth_addresses, then try to compare (index,amount) pairs, else raise error
if self.stealth_address and other.stealth_address:
return self.stealth_address == other.stealth_address
elif None not in (self.index, other.index, self.amount, other.amount):
return self.index == other.index and self.amount == other.amount
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(out2.transaction, self.tx2)
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.assertEqual(self.oto1, OneTimeOutput(index=25973289, amount=Decimal('0.000000000000')))

Loading…
Cancel
Save