Skip to content
Snippets Groups Projects
Commit e5ba6893 authored by Agustin Dorda's avatar Agustin Dorda
Browse files

Merge branch 'desarrollo' into 'master'

Desarrollo

See merge request Blockchain/TsaAPI!6
parents 6c725773 a94829ea
No related branches found
No related tags found
No related merge requests found
import logging
from django.core.management import BaseCommand
from app.services import TransactionUnstucker, MemcachedStorage
from TsaApi.settings import GAS_PRICE
class Command(BaseCommand):
unstucker = TransactionUnstucker()
logger = logging.getLogger('cmd')
storage = MemcachedStorage()
def handle(self, *args, **options):
print(self.storage.retrieve())
import logging
from django.core.management import BaseCommand
from app.managers import EthereumGateway
from app.services import TransactionUnstucker
from TsaApi.settings import GAS_PRICE
import logging
class Command(BaseCommand):
unstucker = TransactionUnstucker(logging.getLogger('console-logger'))
def handle(self, *args, **options):
self.unstucker.unstuck_pending_transactions(GAS_PRICE)
...@@ -16,6 +16,8 @@ from TsaApi.settings import HOST_ADDRESS ...@@ -16,6 +16,8 @@ from TsaApi.settings import HOST_ADDRESS
from web3.middleware import geth_poa_middleware from web3.middleware import geth_poa_middleware
import requests import requests
from app.utils import Utils
class TimestampManager(models.Manager): class TimestampManager(models.Manager):
...@@ -43,7 +45,8 @@ class TimestampManager(models.Manager): ...@@ -43,7 +45,8 @@ class TimestampManager(models.Manager):
def get_contract(contract_version): def get_contract(contract_version):
web3 = TimestampManager.get_provider() web3 = TimestampManager.get_provider()
return web3.eth.contract(abi=CONTRACTS[contract_version]['abi'], address=Web3.toChecksumAddress(CONTRACTS[contract_version]['address'])) return web3.eth.contract(abi=CONTRACTS[contract_version]['abi'],
address=Web3.toChecksumAddress(CONTRACTS[contract_version]['address']))
@staticmethod @staticmethod
def get_block(block_number): def get_block(block_number):
...@@ -61,11 +64,11 @@ class TimestampManager(models.Manager): ...@@ -61,11 +64,11 @@ class TimestampManager(models.Manager):
def stamp(proof_hash, file_hash): def stamp(proof_hash, file_hash):
contract = TimestampManager.get_current_contract() contract = TimestampManager.get_current_contract()
web3 = TimestampManager.get_provider()
return contract.functions.stamp(proof_hash, file_hash).transact({'from': Web3.toChecksumAddress(ACCOUNT_ADDRESS), 'gas': GAS, 'gasPrice': web3.eth.gasPrice}) web3 = TimestampManager.get_provider()
return contract.functions.stamp(proof_hash, file_hash).transact(
{'from': Web3.toChecksumAddress(ACCOUNT_ADDRESS), 'gas': GAS, 'gasPrice': web3.eth.gasPrice})
@staticmethod @staticmethod
def verify(contract_version, proof_hash, file_hash): def verify(contract_version, proof_hash, file_hash):
...@@ -82,11 +85,57 @@ class TimestampManager(models.Manager): ...@@ -82,11 +85,57 @@ class TimestampManager(models.Manager):
headers = {'Content-Type': 'application/json', 'accept': 'application/json'} headers = {'Content-Type': 'application/json', 'accept': 'application/json'}
response = requests.post(HOST_ADDRESS, data='{"jsonrpc":"2.0","method":"clique_getSigners","params":[],"id":1}', headers=headers) response = requests.post(HOST_ADDRESS, data='{"jsonrpc":"2.0","method":"clique_getSigners","params":[],"id":1}',
headers=headers)
return len(response.json()['result']) return len(response.json()['result'])
@staticmethod @staticmethod
def get_last_block_number(): def get_last_block_number():
web3 = TimestampManager.get_provider() web3 = TimestampManager.get_provider()
return web3.eth.getBlock('latest').number return web3.eth.getBlock('latest').number
\ No newline at end of file
class TxAlreadySealedInBlock(Exception):
pass
class EthereumGateway:
def transaction_is_canonical(self, transaction):
last_block_number = TimestampManager.get_last_block_number()
tx_block_number = transaction.blockNumber
required_block_difference = Utils.required_block_difference(TimestampManager.get_signers_count())
return (last_block_number - tx_block_number) > required_block_difference
def transaction(self, tx_hash):
return TimestampManager.get_transaction(tx_hash)
def pending_transactions(self):
w3 = TimestampManager.get_provider()
filt = w3.eth.filter('pending')
tx_hashes = w3.eth.getFilterChanges(filt.filter_id)
# ojo con lo de abajo, es costoso en tiempo, hace len(tx_hashes) conexiones al nodo, si es sobre HTTP es lento
txs = [w3.eth.getTransaction(i.hex()) for i in tx_hashes]
return txs
def block(self, block_number):
return TimestampManager.get_block(block_number)
def resend(self, tx, gas_price=None, gas=None):
w3 = TimestampManager.get_provider()
tx_2 = w3.eth.getTransaction(tx['hash'])
if tx_2['blockNumber'] is not None:
raise TxAlreadySealedInBlock("Tx already sealed in block {}".format(str(tx_2['blockNumber'])))
return w3.eth.resend(tx, gas_price, gas)
class TimeStamp:
def abi(self):
return CONTRACTS[CURRENT_CONTRACT_VERSION]['abi']
def verify(self, proof_hash, file_hash):
return TimestampManager.verify(CURRENT_CONTRACT_VERSION, proof_hash, file_hash)
def get_block(self, proof_hash):
return TimestampManager.get_block_number(CURRENT_CONTRACT_VERSION, proof_hash)
No preview for this file type
import base64
from json import JSONDecodeError
from app.managers import TimestampManager, EthereumGateway, TimeStamp
from app.utils import Utils, Base64EncodingService
from pymemcache.client import base
from TsaApi.settings import MEMCACHED_HOST, MEMCACHED_PORT, PENDING_TXS_MEMCACHED_KEY
import logging
import json
from sys import stdout
from django.utils.translation import gettext as _, gettext
if stdout.isatty():
log_str = 'console-logger'
else:
log_str = 'logger'
module_logger = logging.getLogger(log_str)
class ReceiptInterpreter:
def interpret(self, ots):
return ots.split('-')
class DefinitiveReceiptGenerator:
encoder = Base64EncodingService()
interpreter = ReceiptInterpreter()
gateway = EthereumGateway()
def generate_definitive_receipt(self, original_file_hash, tx_hash, ots_hash):
tx = self.gateway.transaction(tx_hash)
return self.encoder.encode(Utils.get_permanent_ots(original_file_hash, ots_hash, tx_hash,
tx['blockNumber']).encode('utf-8')).decode('utf-8')
class TransactionInputVerification:
gateway = EthereumGateway()
interpreter = ReceiptInterpreter()
ts = TimeStamp()
tx_hash = ''
def verify(self, original_file_hash, ots):
ots_version, file_hash, ots_hash, tx_hash, block_number = self.interpreter.interpret(ots)
method_name, args = Utils.decode_contract_call(self.ts.abi(), TimestampManager.get_transaction(tx_hash).input)
return args[0].decode('utf-8') == ots_hash and args[1].decode('utf-8') == file_hash
class SmartContractVerification:
timestamp = TimeStamp()
interpreter = ReceiptInterpreter()
gateway = EthereumGateway()
def verify(self, file_hash, ots_hash):
verified = self.timestamp.verify(ots_hash, file_hash)
block = None
if verified:
block_number = self.timestamp.get_block(ots_hash)
block = self.gateway.block(block_number)
return verified, block
class EventVerification:
gateway = EthereumGateway()
class VerifyService:
receipt_generator = DefinitiveReceiptGenerator()
definitive_verificator = SmartContractVerification()
temp_verificator = SmartContractVerification()
interpreter = ReceiptInterpreter()
gateway = EthereumGateway()
permanent_ots_prefix = ''
def verify(self, original_file_hash, rd):
if rd[:2] == self.permanent_ots_prefix:
ots_version, file_hash, ots_hash, tx_hash, block_number = self.interpreter.interpret(rd)
verified, block = self.definitive_verificator.verify(original_file_hash, ots_hash)
if verified:
return {'status': 'success',
'block': block,
'permanent_rd': self.receipt_generator.generate_definitive_receipt(original_file_hash,
tx_hash,
ots_hash)}
else:
return {'status': 'failure'}
else:
ots_version, ots_hash, tx_hash = self.interpreter.interpret(rd)
verified, block = self.temp_verificator.verify(original_file_hash, ots_hash)
tx = self.gateway.transaction(tx_hash)
if verified:
if not self.gateway.transaction_is_canonical(tx):
return {'status': 'pending'}
else:
return {'status': 'success',
'block': block,
'permanent_rd': self.receipt_generator.generate_definitive_receipt(original_file_hash,
tx_hash,
ots_hash)}
else:
try:
if tx and not tx.blockNumber:
return {'status': 'pending'}
except ValueError:
pass
return {'status': 'failure'}
class StatusNotValidException(Exception):
pass
class VerifyServiceTranslation:
verify_service = VerifyService()
def verify(self, original_file_hash, rd):
result = self.verify_service.verify(original_file_hash, rd)
dict_res = {gettext('status'): result['status']}
res_status = 200
if result['status'] == 'success':
dict_res[gettext('permanent_rd')] = result['permanent_rd']
dict_res[gettext('attestation_time')] = str(Utils.datetime_from_timestamp(result['block'].timestamp))
dict_res[gettext('messages')] = gettext('file_uploaded') % (
original_file_hash,
str(result['block'].number),
str(Utils.datetime_from_timestamp(result['block'].timestamp))
)
elif result['status'] == 'failure':
dict_res[gettext('messages')] = gettext('file_not_found')
res_status = 404
elif result['status'] == 'pending':
dict_res[gettext('messages')] = gettext('transaction_pending')
else:
raise StatusNotValidException('Status invalid in verify service result : {}'.format(result['status']))
return dict_res, res_status
class MemcachedStorage:
client = base.Client((MEMCACHED_HOST, MEMCACHED_PORT))
server_tuple = (MEMCACHED_HOST, MEMCACHED_PORT)
key = PENDING_TXS_MEMCACHED_KEY
logger = module_logger
def store(self, pending_txs):
jsoned = json.dumps(pending_txs)
self.client.set(self.key, jsoned)
def retrieve(self):
retrieved = self.client.get(self.key)
try:
decoded = retrieved.decode("utf-8")
interpretado = json.loads(decoded)
except AttributeError as ae:
self.logger.info("Memcached estaba vacío key: {} ; valor : {}".format(str(self.key), retrieved))
interpretado = []
except JSONDecodeError as jsonde:
self.logger.error(
"Se devolvera lista vacia. No se pudo decodear la string: {} {}".format(retrieved, str(jsonde)))
interpretado = []
return interpretado
class CheckCriteria:
storage = MemcachedStorage()
def __init__(self, logger=None):
if logger is not None:
self.logger = logger
def should_transactions_be_resent(self, stuck_txs):
stored_txs = self.storage.retrieve()
txs_dict = {}
for stx in stored_txs:
txs_dict[stx['hash']] = stx
res = False
for tx in stuck_txs:
res = res or tx['hash'] in txs_dict
return res
class PendingTransactionsService:
gateway = EthereumGateway()
criteria = CheckCriteria()
logger = module_logger
def __init__(self, logger=None):
if logger is not None:
self.logger = logger
def resend_transactions(self, txs, new_gas_price, new_gas_limit=None):
self.logger.info("Por evaluar si deberían ser reenviadas las transacciones")
new_txs = []
failed = []
if self.criteria.should_transactions_be_resent(txs): # obs: si es vacío entonces evalúa True
for tx in txs: # si es vacío este ciclo no hace nada
self.logger.info("Reenviando transaccion {}".format(str(tx)))
try:
new_txs.append(self.gateway.resend(tx, new_gas_price, new_gas_limit))
except Exception as e:
failed.append(tx)
self.logger.error('Error :{}'.format(str(e)))
return new_txs, failed
class TransactionUnstucker:
pending_transaction_service = PendingTransactionsService()
gateway = EthereumGateway()
storage = MemcachedStorage()
logger = module_logger
def __init__(self, logger=None):
if logger is not None:
self.logger = logger
self.pending_transaction_service.logger = logger
def unstuck_pending_transactions(self, new_gas_price, new_gas=None):
self.logger.info('Arranca proceso')
stuck_txs = self.gateway.pending_transactions()
self.logger.debug('Cant txs encontradas : {}'.format(len(stuck_txs)))
res, failed = self.pending_transaction_service.resend_transactions(stuck_txs, new_gas_price, new_gas)
self.logger.debug(
'Terminado, cant reenvios con exito: {} ; cant reenvios fallados: {}'.format(len(res), len(failed)))
nuevas_txs = self.gateway.pending_transactions()
self.logger.debug('Cant txs encontradas : {}'.format(len(nuevas_txs)))
self.storage.store(nuevas_txs)
self.logger.info('Termina proceso')
return res, failed
...@@ -7,28 +7,453 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY ...@@ -7,28 +7,453 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/ You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/
""" """
import base64
import json
import time
import unittest
from django.utils.translation import gettext as _
from django.test import TestCase from django.test import TestCase
from django.test import Client from django.test import Client
from eth_account.datastructures import AttributeDict
from pymemcache.client import base
from rest_framework import status from rest_framework import status
from TsaApi.settings import TEST_MEMCACHED_HOST, TEST_MEMCACHED_PORT, TEST_PENDING_TXS_MEMCACHED_KEY, \
PERMANENT_OTS_PREFIX
from app.services import PendingTransactionsService, CheckCriteria, MemcachedStorage, TransactionUnstucker, \
VerifyService
class TimestampTest(TestCase):
def test_verify(self): class TimestampTest(unittest.TestCase):
def test_full_cycle(self):
c = Client() c = Client()
response = c.post('/api/tsa/verify/', { response = c.post('/api/tsa/stamp/', {
"file_hash": "1957db7fe23e4be1740ddeb941ddda7ae0a6b782e536a9e00b5aa82db1e84547"
})
self.assertEqual(response.status_code, status.HTTP_200_OK, 'Stamp Error')
dict_response = json.loads(response.content.decode())
time.sleep(30)
verify_response = c.post('/api/tsa/verify/', {
"file_hash": "1957db7fe23e4be1740ddeb941ddda7ae0a6b782e536a9e00b5aa82db1e84547", "file_hash": "1957db7fe23e4be1740ddeb941ddda7ae0a6b782e536a9e00b5aa82db1e84547",
"proof": "NjA2MWQ1ODNkMTM0YTlmZGZlYWYxMWE2MjY1ZDY1OTkwMGM0ZGZjNjM2MDk3MDY4N2VkNDk3NTNhZmE5MGM3YzAxLTB4NWRiYjg4NjNlNDlhN2NmYTY0N2FmYzVkOTFkMWRiMDUxY2YzODZiYWY4NzJkM2ZiYTIzN2JkY2IwOWUwNWMzYQ==" "rd": dict_response['temporary_rd']
}) })
self.assertEqual(response.status_code, status.HTTP_200_OK, 'Verify Error') self.assertEqual(verify_response.status_code, status.HTTP_200_OK, 'Verify Error')
dict_verify = json.loads(verify_response.content.decode())
self.assertEqual(dict_verify[_('status')], _('success'))
def test_stamp(self):
c = Client() class EthereumMocks:
response = c.post('/api/tsa/stamp/', { class HalfResender:
"file_hash": "1957db7fe23e4be1740ddeb941ddda7ae0a6b782e536a9e00b5aa82db1e84547" balance = 2.1 * 21000 * 1000 # me da para 2 txs mockeadas
})
def resend(self, tx, gas_price, gas=None):
if gas is None:
gas = tx['gas']
costo = (gas_price * gas) + tx['value']
if self.balance > costo:
self.balance = self.balance - costo
return tx
raise Exception("Failing test")
class EthereumMock:
def __init__(self):
self.pending_txs = [
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': None,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 1000,
}),
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': 0,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x6c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 1000,
}),
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': 0,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x7d515ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 1000,
})
]
def resend(self, tx, gas_price, gas):
self.pending_txs.remove(tx)
return tx
def pending_transactions(self):
return self.pending_txs.copy()
class CriteriaMockTrue:
def should_transactions_be_resent(self, txs):
return True
class CriteriaMockFalse:
def should_transactions_be_resent(self, txs):
return False
class StorageMock:
def __init__(self, stored_txs):
self.stored_txs = stored_txs
def store(self, txs):
self.stored_txs = txs
def retrieve(self):
return self.stored_txs
class FailResender:
def resend(self, tx, gas_price, gas):
raise Exception("Failing test")
def generar_valores(self):
self.current_txs = \
[
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': None,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 1000,
}),
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': 0,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x6c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 1000,
}),
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': 0,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x7d515ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 1000,
})
]
self.stored_txs = \
[
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': None,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 31337,
}),
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': 0,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x6c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 31337,
}),
AttributeDict({
'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd',
'blockNumber': 0,
'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4',
'gas': 21000,
'gasPrice': 500,
'hash': '0x6c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060',
'input': '0x',
'nonce': 0,
'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734',
'transactionIndex': 0,
'value': 31337,
})
]
class PendingTransactionTest(unittest.TestCase, EthereumMocks):
def setUp(self):
self.service = PendingTransactionsService()
self.generar_valores()
def test_verify_criteria_affirmative(self):
self.service.criteria = self.CriteriaMockTrue()
self.service.gateway = self.EthereumMock()
mock_txs = self.current_txs
result, failed = self.service.resend_transactions(mock_txs, 1000, )
self.assertEqual(result, mock_txs)
self.assertEqual(failed, [])
def test_verify_criteria_false(self):
self.service.criteria = self.CriteriaMockFalse()
self.service.gateway = self.EthereumMock()
mock_txs = self.current_txs
result, failed = self.service.resend_transactions(mock_txs, 1000, )
self.assertEqual(result, [])
self.assertEqual(failed, [])
def test_resend(self):
criteria = CheckCriteria()
criteria.storage = self.StorageMock(self.stored_txs)
self.service.criteria = criteria
self.service.gateway = self.EthereumMock()
txs = self.current_txs
result, failed = self.service.resend_transactions(txs, 1000, )
self.assertEqual(result, txs)
self.assertEqual(failed, [])
def test_resend_everything_failed(self):
criteria = CheckCriteria()
criteria.storage = self.StorageMock(self.stored_txs)
self.service.criteria = criteria
self.service.gateway = self.FailResender()
txs = self.current_txs
result, failed = self.service.resend_transactions(txs, 1000, )
self.assertEqual(result, [])
self.assertEqual(failed, txs)
def test_resend_some_failed(self):
criteria = CheckCriteria()
criteria.storage = self.StorageMock(self.stored_txs)
self.service.criteria = criteria
self.service.gateway = self.HalfResender()
txs = self.current_txs
result, failed = self.service.resend_transactions(txs, 1000, )
self.assertEqual(result, txs[:2])
self.assertEqual(failed, [txs[-1]])
class MemcachedStorageTest(unittest.TestCase):
def setUp(self):
self.storage = MemcachedStorage()
pass
class MemcachedMock:
dictionary = {}
def __init__(self, some=None): # necesario para el monkey patching que hice en un test
pass
def set(self, key, value, expire_time=0):
self.dictionary[key] = str(value).encode()
return value
def get(self, key):
return self.dictionary[key]
def test_mock_memcached(self):
old_client = self.storage.client
old_mem_client = base.Client # perdon por el monkey patching :c
self.storage.client = self.MemcachedMock()
base.Client = self.MemcachedMock
some_stuff = [123, 345, 78, 9]
self.storage.store(some_stuff)
retrieved = self.storage.retrieve()
self.assertEqual(some_stuff, retrieved)
self.storage.client = old_client
base.Client = old_mem_client
def test_integrated_memcached(self):
m_client = base.Client((TEST_MEMCACHED_HOST, TEST_MEMCACHED_PORT))
m_client.set(TEST_PENDING_TXS_MEMCACHED_KEY, [])
self.storage.client = m_client
self.storage.key = TEST_PENDING_TXS_MEMCACHED_KEY
some_stuff = [1, 2, 3, 4, 54, 5, 765]
self.storage.store(some_stuff)
retrieved = self.storage.retrieve()
self.assertEqual(some_stuff, retrieved)
class TransactionUnstuckerTest(unittest.TestCase, EthereumMocks):
def setUp(self):
self.unstucker = TransactionUnstucker()
self.generar_valores()
pass
def test_unstuck_mock_storage(self):
mock = self.EthereumMock()
service = PendingTransactionsService()
service.criteria.storage = self.StorageMock(mock.pending_transactions())
service.gateway = mock
self.unstucker.gateway = mock
self.unstucker.pending_transaction_service = service
self.unstucker.storage = service.criteria.storage
res, failed = self.unstucker.unstuck_pending_transactions(1000)
self.assertEqual(failed, [])
self.assertEqual(mock.pending_transactions(), [])
# testeo contra transacciones cacheadas que siguen pendientes,
def test_unstuck_integrated_memcached_custom_client(self):
m_client = base.Client((TEST_MEMCACHED_HOST, TEST_MEMCACHED_PORT)) # me hago mi propio cliente
m_client.set(TEST_PENDING_TXS_MEMCACHED_KEY, []) # limpio por las dudas sobre otra key
mock = self.EthereumMock() # mockeo el gateway de ethereum
service = PendingTransactionsService()
service.criteria.storage.client = m_client # setteo mi cliente
service.criteria.storage.key = TEST_PENDING_TXS_MEMCACHED_KEY
service.criteria.storage.store(mock.pending_transactions()) # y le asigno las pendientes mockeadas
service.gateway = mock # asigno los mocks que armé
self.unstucker.gateway = mock
self.unstucker.pending_transaction_service = service
self.unstucker.storage = service.criteria.storage
res, failed = self.unstucker.unstuck_pending_transactions(1000)
self.assertEqual(failed, [])
self.assertEqual(res, self.current_txs)
self.assertEqual(mock.pending_transactions(), [])
# testeo contra nada de transacciones cacheadas, y transacciones pendientes en el momento
def test_unstuck_integrated_memcached_custom_client_nothing_to_do(self):
m_client = base.Client((TEST_MEMCACHED_HOST, TEST_MEMCACHED_PORT))
m_client.set(TEST_PENDING_TXS_MEMCACHED_KEY, [])
mock = self.EthereumMock()
service = PendingTransactionsService()
service.criteria.storage.client = m_client
service.gateway = mock
self.unstucker.gateway = mock
self.unstucker.pending_transaction_service = service
self.unstucker.storage = service.criteria.storage
res, failed = self.unstucker.unstuck_pending_transactions(1000)
self.assertEqual(failed, [])
self.assertEqual(res, []) # como no tenía cache entonces no se hizo nada
class VerificationMocks:
class NotSealedTransactionMock:
def transaction(self, tx_hash):
return AttributeDict(
{'hash': tx_hash, 'blockNumber': None})
class SealedTransactionMock:
def transaction(self, tx_hash):
return AttributeDict(
{'hash': tx_hash, 'blockNumber': 1000})
class TXisCanonicalMock:
def transaction_is_canonical(self, tx):
return True
class TXisNotCanonicalMock:
def transaction_is_canonical(self, tx):
return False
class FalseVerificationMock:
def verify(self, file_hash, ots_hash):
return False, None
class TrueVerificationMock:
def verify(self, file_hash, ots_hash):
return True, AttributeDict({'number': 1000})
class TxCanonicalSealedTx(SealedTransactionMock, TXisCanonicalMock):
pass
class TxNoCanonicalSealedTx(SealedTransactionMock, TXisNotCanonicalMock):
pass
class ReceiptGeneratorMock:
def generate_definitive_receipt(self, original_file_hash, tx_hash, ots_hash):
return "{}{}{}".format(original_file_hash, tx_hash, ots_hash)
class VerifyServiceTest(unittest.TestCase, VerificationMocks):
def setUp(self):
self.service = VerifyService()
self.service.permanent_ots_prefix = PERMANENT_OTS_PREFIX
self.ots = "d6b88be854cb88cf1e161d8bbd912987cff236eb43a3a15fff86370054dd4d3f01"
self.file_hash = 'some_hash'
self.contract_version = '01'
self.temp_rd = "0x-d6b88be854cb88cf1e161d8bbd912987cff236eb43a3a15fff86370054dd4d3f01-0xf495705b5d7d68afdcefe72d9bbacec59b1a968c964ab0b6b13cec6491e5ddec"
self.def_rd = "1x-7b58a42ee27c3699fc169ce43cb4ba598003cba5699f4cd518b91d4177f61f10-0577d1390a5733534c7b6cc45a7ba15d0b5bf37faec9c54f749826af75aa86bb01-0x1a3a5335b44bdb7f62201102e91658586fd1d7845c621503eeff7781c3020b09-2234445"
pass
def test_verify_temporary_rd_not_sealed(self):
self.service.gateway = self.NotSealedTransactionMock()
self.service.temp_verificator = self.FalseVerificationMock()
res = self.service.verify(self.file_hash, self.temp_rd)
self.assertEqual(res['status'], 'pending')
def test_verify_temporary_rd_sealed_not_canonical(self):
self.service.gateway = self.TxNoCanonicalSealedTx()
self.service.temp_verificator = self.TrueVerificationMock()
res = self.service.verify(self.file_hash, self.temp_rd)
self.assertEqual(res['status'], 'pending')
def test_verify_temporary_rd_sealed_canonical(self):
self.service.temp_verificator = self.TrueVerificationMock()
self.service.gateway = self.TxCanonicalSealedTx()
self.service.receipt_generator.gateway = self.SealedTransactionMock()
res = self.service.verify(self.file_hash, self.temp_rd)
self.assertEqual(res['status'], 'success')
rd = res['permanent_rd']
ots_version, file_hash, ots_hash, tx_hash, block_number = base64.b64decode(rd).decode().split('-')
self.assertEqual(ots_version, PERMANENT_OTS_PREFIX)
self.assertEqual(file_hash, self.file_hash)
self.assertEqual(ots_hash, self.ots)
self.assertEqual(res['block'].number, int(block_number))
def test_definitive_rd_correct(self):
self.service.definitive_verificator = self.TrueVerificationMock()
self.service.receipt_generator = self.ReceiptGeneratorMock()
res = self.service.verify(self.file_hash, self.def_rd)
self.assertEqual(res['status'], 'success')
self.assertEqual(response.status_code, status.HTTP_200_OK, 'Stamp Error') def test_definitive_rd_incorrect(self):
\ No newline at end of file res = self.service.verify(self.file_hash, self.def_rd)
self.assertEqual(res['status'], 'failure')
...@@ -7,16 +7,15 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY ...@@ -7,16 +7,15 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/ You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/
""" """
import base64
import datetime import datetime
import hashlib import hashlib
import time import time
import math import math
from TsaApi.settings import ACCOUNT_ADDRESS, CURRENT_CONTRACT_VERSION, PERMANENT_OTS_PREFIX, TEMPORARY_OTS_PREFIX from TsaApi.settings import ACCOUNT_ADDRESS, CURRENT_CONTRACT_VERSION, PERMANENT_OTS_PREFIX, TEMPORARY_OTS_PREFIX
from ethereum.abi import (decode_abi, normalize_name as normalize_abi_method_name, method_id as get_abi_method_id)
from ethereum.utils import encode_int, zpad, decode_hex
class Utils(): class Utils:
@staticmethod @staticmethod
def sha256_encode(string): def sha256_encode(string):
return hashlib.sha256(string.encode('utf-8')).hexdigest() return hashlib.sha256(string.encode('utf-8')).hexdigest()
...@@ -27,7 +26,8 @@ class Utils(): ...@@ -27,7 +26,8 @@ class Utils():
@staticmethod @staticmethod
def get_ots_hash(file_hash): def get_ots_hash(file_hash):
#El ots se genera con los hashes sha256 de (archivo original+timestamp+dirección de la cuenta) + versión del contrato # El ots se genera con los hashes sha256 de (archivo original+timestamp+dirección de la cuenta) + versión del
# contrato
return Utils.sha256_encode(str(file_hash + Utils.sha256_encode(str(int(time.time()))) + Utils.sha256_encode(ACCOUNT_ADDRESS))) + CURRENT_CONTRACT_VERSION return Utils.sha256_encode(str(file_hash + Utils.sha256_encode(str(int(time.time()))) + Utils.sha256_encode(ACCOUNT_ADDRESS))) + CURRENT_CONTRACT_VERSION
@staticmethod @staticmethod
...@@ -58,4 +58,9 @@ class Utils(): ...@@ -58,4 +58,9 @@ class Utils():
@staticmethod @staticmethod
def get_temporary_ots(ots_hash, tx_hash): def get_temporary_ots(ots_hash, tx_hash):
return TEMPORARY_OTS_PREFIX + '-' + ots_hash + '-' + tx_hash return TEMPORARY_OTS_PREFIX + '-' + ots_hash + '-' + tx_hash
\ No newline at end of file
class Base64EncodingService:
def encode(self, stuff):
return base64.b64encode(stuff)
...@@ -8,19 +8,26 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY ...@@ -8,19 +8,26 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/ You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/
""" """
import base64 import base64
import logging
import coreapi
import coreschema
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from raven.contrib.django.raven_compat.models import client
from rest_framework import status from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from web3.exceptions import CannotHandleRequest
from raven.contrib.django.raven_compat.models import client
from rest_framework.schemas import ManualSchema from rest_framework.schemas import ManualSchema
import coreschema,coreapi from rest_framework.views import APIView
from web3.exceptions import CannotHandleRequest
from TsaApi.local_settings import PERMANENT_OTS_PREFIX
from app.managers import TimestampManager from app.managers import TimestampManager
from app.services import VerifyServiceTranslation
from app.utils import Utils from app.utils import Utils
from TsaApi.local_settings import TEMPORARY_OTS_PREFIX, PERMANENT_OTS_PREFIX, CONTRACTS
logger = logging.getLogger('logger')
class Stamp(APIView): class Stamp(APIView):
""" """
...@@ -47,7 +54,6 @@ class Stamp(APIView): ...@@ -47,7 +54,6 @@ class Stamp(APIView):
schema=coreschema.String(), schema=coreschema.String(),
description='El hash del archivo encodeado en sha256', description='El hash del archivo encodeado en sha256',
), ),
]) ])
...@@ -61,20 +67,26 @@ class Stamp(APIView): ...@@ -61,20 +67,26 @@ class Stamp(APIView):
ots_hash = Utils.get_ots_hash(file_hash) ots_hash = Utils.get_ots_hash(file_hash)
logger.info("Haciendo stamp para hash: {} ots: {} ".format(file_hash, ots_hash))
tx_hash = TimestampManager.stamp(ots_hash, file_hash) tx_hash = TimestampManager.stamp(ots_hash, file_hash)
logger.info("Stamp para hash: {} ots: {} terminado, tx_hash : {}".format(file_hash, ots_hash, tx_hash.hex()))
#Al OTS se le agrega la transacción para poder verificar luego si está pendiente de subida # Al OTS se le agrega la transacción para poder verificar luego si está pendiente de subida
ots = Utils.get_temporary_ots(ots_hash, tx_hash.hex()) ots = Utils.get_temporary_ots(ots_hash, tx_hash.hex())
return Response({_('status'): _('success'), _('temporary_rd'): base64.b64encode(ots.encode('utf-8')).decode('utf-8')}, status=status.HTTP_200_OK) return Response(
{_('status'): _('success'), _('temporary_rd'): base64.b64encode(ots.encode('utf-8')).decode('utf-8')},
status=status.HTTP_200_OK)
except ValidationError as e: except ValidationError as e:
return Response({_('status'): _('failure'), _('messages'): _('parameter_missing') % e.message}, status=status.HTTP_400_BAD_REQUEST) return Response({_('status'): _('failure'), _('messages'): _('parameter_missing') % e.message},
status=status.HTTP_400_BAD_REQUEST)
except CannotHandleRequest: except CannotHandleRequest:
return Response({_('status'): _('failure'), _('messages'): _('could_not_connect')}, status=status.HTTP_503_SERVICE_UNAVAILABLE) return Response({_('status'): _('failure'), _('messages'): _('could_not_connect')},
status=status.HTTP_503_SERVICE_UNAVAILABLE)
except Exception as e: except Exception as e:
client.captureException() client.captureException()
return Response({_('status'): _('failure'), _('messages'): _('operation_failed')}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({_('status'): _('failure'), _('messages'): _('operation_failed')},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class Verify(APIView): class Verify(APIView):
...@@ -112,6 +124,8 @@ class Verify(APIView): ...@@ -112,6 +124,8 @@ class Verify(APIView):
) )
]) ])
verify_service = VerifyServiceTranslation()
def post(self, request): def post(self, request):
try: try:
...@@ -122,65 +136,26 @@ class Verify(APIView): ...@@ -122,65 +136,26 @@ class Verify(APIView):
raise ValidationError('rd') raise ValidationError('rd')
original_file_hash = request.data.get('file_hash') original_file_hash = request.data.get('file_hash')
base64_ots = request.data.get('rd') base64_rd = request.data.get('rd')
ots = base64.b64decode(base64_ots).decode('utf-8')
if ots[:2] == PERMANENT_OTS_PREFIX:
ots_version, file_hash, ots_hash, tx_hash, block_number = ots.split('-') rd = base64.b64decode(base64_rd).decode('utf-8')
method_name, args = Utils.decode_contract_call(CONTRACTS['01']['abi'], TimestampManager.get_transaction(tx_hash).input)
if args[0].decode('utf-8') == ots_hash and args[1].decode('utf-8') == original_file_hash:
block = TimestampManager.get_block(int(block_number))
return Response({_('status'): _('success'),
_('permanent_rd'): base64.b64encode(Utils.get_permanent_ots(original_file_hash, ots_hash, tx_hash, block.number).encode('utf-8')).decode('utf-8'),
_('attestation_time'): str(Utils.datetime_from_timestamp(block.timestamp)),
_('messages'): _('file_uploaded') % (
file_hash, str(block.number), str(Utils.datetime_from_timestamp(block.timestamp)))},
status=status.HTTP_200_OK)
else:
return Response({_('status'): _('failure'), _('messages'): _('file_not_found')},
status=status.HTTP_404_NOT_FOUND)
else:
ots_version, ots_hash, tx_hash = ots.split('-')
transaction = TimestampManager.get_transaction(tx_hash)
contract_version = ots_hash[-2:]
if TimestampManager.verify(contract_version, ots_hash, original_file_hash):
if (TimestampManager.get_last_block_number() - transaction.blockNumber) < Utils.required_block_difference(TimestampManager.get_signers_count()):
return Response({_('status'): _('pending'), _('messages'): _('transaction_pending')},
status=status.HTTP_200_OK)
else:
block = TimestampManager.get_block(TimestampManager.get_block_number(contract_version, ots_hash))
return Response({_('status'): _('success'),
_('permanent_rd'): base64.b64encode(Utils.get_permanent_ots(original_file_hash, ots_hash, tx_hash, block.number).encode('utf-8')).decode('utf-8'),
_('attestation_time'): str(Utils.datetime_from_timestamp(block.timestamp)),
_('messages'): _('file_uploaded') % (original_file_hash, str(block.number), str(
Utils.datetime_from_timestamp(block.timestamp)))}, status=status.HTTP_200_OK)
else:
try:
if transaction and not transaction.blockNumber:
return Response({_('status'): _('pending'), _('messages'): _('transaction_pending')}, status=status.HTTP_200_OK)
except ValueError:
pass
return Response({_('status'): _('failure'), _('messages'): _('file_not_found')},status=status.HTTP_404_NOT_FOUND)
# importante la asignación del prefijo de ots permanente
self.verify_service.verify_service.permanent_ots_prefix = PERMANENT_OTS_PREFIX
logger.info("Iniciando verificacion para hash {} rd {} baserd {}".format(original_file_hash, rd, base64_rd))
result, res_status = self.verify_service.verify(original_file_hash, rd)
logger.info(
"Verificacion terminada para hash {} rd {}, resultado {}".format(original_file_hash, rd, str(result)))
return Response(result, status=res_status)
except ValidationError as e: except ValidationError as e:
return Response({_('status'): _('failure'), _('messages'): _('parameter_missing') % e.message}, status=status.HTTP_400_BAD_REQUEST) return Response({_('status'): _('failure'), _('messages'): _('parameter_missing') % e.message},
except CannotHandleRequest: status=status.HTTP_400_BAD_REQUEST)
return Response({_('status'): _('failure'), _('messages'): _('could_not_connect')}, status=status.HTTP_503_SERVICE_UNAVAILABLE) except CannotHandleRequest as e:
logger.critical('Servicio a nivel Geth no disponible!! error: {}'.format(str(e)))
return Response({_('status'): _('failure'), _('messages'): _('could_not_connect')},
status=status.HTTP_503_SERVICE_UNAVAILABLE)
except Exception as e: except Exception as e:
client.captureException() client.captureException()
return Response({_('status'): _('failure'), _('messages'): _('operation_failed')}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) logger.error('Error interno en la verificacion : {}'.format(str(e)))
\ No newline at end of file return Response({_('status'): _('failure'), _('messages'): _('operation_failed')},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
pyethereum @ d962694b
Subproject commit d962694be03686a8e5c1d7459ae272b70a5c9f77
...@@ -30,7 +30,6 @@ MarkupSafe==1.0 ...@@ -30,7 +30,6 @@ MarkupSafe==1.0
openapi-codec==1.3.2 openapi-codec==1.3.2
parsimonious==0.8.0 parsimonious==0.8.0
pbkdf2==1.3 pbkdf2==1.3
pkg-resources==0.0.0
py-ecc==1.4.3 py-ecc==1.4.3
pycparser==2.18 pycparser==2.18
pycryptodome==3.6.4 pycryptodome==3.6.4
...@@ -52,4 +51,7 @@ uritemplate==3.0.0 ...@@ -52,4 +51,7 @@ uritemplate==3.0.0
urllib3==1.23 urllib3==1.23
web3==4.5.0 web3==4.5.0
websockets==5.0.1 websockets==5.0.1
elastic-apm==3.0.0
ipython==7.2.0
ipython-genutils==0.2.0
pymemcache==2.1.1
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment