From 27c006e791d6265818672bedebef46ec7f0379a5 Mon Sep 17 00:00:00 2001 From: Dzmitry Aliksandrau Date: Thu, 24 Nov 2022 22:41:57 +0400 Subject: [PATCH] Read bulk_payout logs to avoid double payouts --- Pipfile | 1 + Pipfile.lock | 29 ++++++++++++++++++++++++- hmt_escrow/eth_bridge.py | 32 ++++++++++++++++++++++++++++ hmt_escrow/job.py | 42 +++++++++++++++++++++++++++++++++---- package-lock.json | 2 +- package.json | 2 +- setup.py | 3 ++- test/hmt_escrow/test_job.py | 34 ++++++++++++++++++++++++------ 8 files changed, 131 insertions(+), 14 deletions(-) diff --git a/Pipfile b/Pipfile index 2ca82735..b908a6f3 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ hmt-basemodels = "==0.1.18" py-solc-x = "*" sphinx = "*" web3 = "==5.24.0" +pysha3 = "==1.0.2" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index f09377e5..5f58ea4e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "609e1138c6ec52ee784aca08c9ac3fb9b0cb92fcf43d538f9eb8f192ee88d0c6" + "sha256": "6127f9e174c109da68471584e492e46108f8624b631a22804c5a397a5d3063f5" }, "pipfile-spec": 6, "requires": { @@ -1018,6 +1018,33 @@ "markers": "python_version >= '3.7'", "version": "==0.18.1" }, + "pysha3": { + "hashes": [ + "sha256:0060a66be16665d90c432f55a0ba1f6480590cfb7d2ad389e688a399183474f0", + "sha256:11a2ba7a2e1d9669d0052fc8fb30f5661caed5512586ecbeeaf6bf9478ab5c48", + "sha256:386998ee83e313b6911327174e088021f9f2061cbfa1651b97629b761e9ef5c4", + "sha256:41be70b06c8775a9e4d4eeb52f2f6a3f356f17539a54eac61f43a29e42fd453d", + "sha256:4416f16b0f1605c25f627966f76873e432971824778b369bd9ce1bb63d6566d9", + "sha256:571a246308a7b63f15f5aa9651f99cf30f2a6acba18eddf28f1510935968b603", + "sha256:59111c08b8f34495575d12e5f2ce3bafb98bea470bc81e70c8b6df99aef0dd2f", + "sha256:5ec8da7c5c70a53b5fa99094af3ba8d343955b212bc346a0d25f6ff75853999f", + "sha256:684cb01d87ed6ff466c135f1c83e7e4042d0fc668fa20619f581e6add1d38d77", + "sha256:68c3a60a39f9179b263d29e221c1bd6e01353178b14323c39cc70593c30f21c5", + "sha256:6e6a84efb7856f5d760ee55cd2b446972cb7b835676065f6c4f694913ea8f8d9", + "sha256:827b308dc025efe9b6b7bae36c2e09ed0118a81f792d888548188e97b9bf9a3d", + "sha256:93abd775dac570cb9951c4e423bcb2bc6303a9d1dc0dc2b7afa2dd401d195b24", + "sha256:9c778fa8b161dc9348dc5cc361e94d54aa5ff18413788f4641f6600d4893a608", + "sha256:9fdd28884c5d0b4edfed269b12badfa07f1c89dbc5c9c66dd279833894a9896b", + "sha256:c7c2adcc43836223680ebdf91f1d3373543dc32747c182c8ca2e02d1b69ce030", + "sha256:c93a2676e6588abcfaecb73eb14485c81c63b94fca2000a811a7b4fb5937b8e8", + "sha256:cd5c961b603bd2e6c2b5ef9976f3238a561c58569945d4165efb9b9383b050ef", + "sha256:f9046d59b3e72aa84f6dae83a040bd1184ebd7fef4e822d38186a8158c89e3cf", + "sha256:fd7e66999060d079e9c0e8893e78d8017dad4f59721f6fe0be6307cd32127a07", + "sha256:fe988e73f2ce6d947220624f04d467faf05f1bbdbc64b0a201296bb3af92739e" + ], + "index": "pypi", + "version": "==1.0.2" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", diff --git a/hmt_escrow/eth_bridge.py b/hmt_escrow/eth_bridge.py index 72553857..8c40105e 100644 --- a/hmt_escrow/eth_bridge.py +++ b/hmt_escrow/eth_bridge.py @@ -12,6 +12,7 @@ from web3.providers.auto import load_provider_from_uri from web3.providers.eth_tester import EthereumTesterProvider from web3.types import TxReceipt +import sha3 from hmt_escrow.kvstore_abi import abi as kvstore_abi @@ -437,3 +438,34 @@ def set_pub_key_at_addr( } return handle_transaction(txn_func, *func_args, **txn_info) + + +def get_entity_topic(contract: str, eventname: str) -> str: + """ + Args: + contract (str): contract name ex.: EscrowFactory.sol:EscrowFactory. + + eventname (str): event name to find in abi. + + Returns + str: returns keccak_256 hash of event name with input parameters. + """ + contract_interface = get_contract_interface( + "{}/{}".format(CONTRACT_FOLDER, contract) + ) + s = "" + + for entity in contract_interface["abi"]: + eventName = entity.get("name") + if eventName == eventname: + s += eventName + "(" + inputs = entity.get("inputs", []) + inputTypes = [] + for input in inputs: + inputTypes.append(input.get("internalType")) + s += ",".join(inputTypes) + ")" + + k = sha3.keccak_256() + k.update(s.encode("utf-8")) + + return k.hexdigest() diff --git a/hmt_escrow/job.py b/hmt_escrow/job.py index 138b3f36..52d7585b 100644 --- a/hmt_escrow/job.py +++ b/hmt_escrow/job.py @@ -15,6 +15,7 @@ from hmt_escrow import utils from hmt_escrow.eth_bridge import ( get_hmtoken, + get_entity_topic, get_escrow, get_factory, deploy_factory, @@ -26,6 +27,7 @@ from hmt_escrow.storage import download, upload, get_public_bucket_url, get_key_from_url GAS_LIMIT = int(os.getenv("GAS_LIMIT", 4712388)) +BULK_TRANSFER_EVENT = get_entity_topic("HMToken.sol:HMToken", "BulkTransfer") # Explicit env variable that will use s3 for storing results. @@ -617,6 +619,7 @@ def bulk_payout( bool: returns True if paying to ethereum addresses and oracles succeeds. """ + bulk_paid = False txn_event = "Bulk payout" txn_func = self.job_contract.functions.bulkPayOut txn_info = { @@ -646,23 +649,35 @@ def bulk_payout( func_args = [eth_addrs, hmt_amounts, url, hash_, 1] try: - handle_transaction_with_retry(txn_func, self.retry, *func_args, **txn_info) - return self._bulk_paid() is True + tx_receipt = handle_transaction_with_retry( + txn_func, + self.retry, + *func_args, + **txn_info, + ) + bulk_paid = self._check_transfer_event(tx_receipt) except Exception as e: LOG.warn( f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv} due to {e}. Using secondary ones..." ) + if bulk_paid: + return bulk_paid + + LOG.warn( + f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv}. Using secondary ones..." + ) + raffle_txn_res = self._raffle_txn( self.multi_credentials, txn_func, func_args, txn_event ) - bulk_paid = raffle_txn_res["txn_succeeded"] + bulk_paid = self._check_transfer_event(raffle_txn_res["tx_receipt"]) if not bulk_paid: LOG.warning(f"{txn_event} failed with all credentials.") - return bulk_paid is True + return bulk_paid def abort(self) -> bool: """Kills the contract and returns the HMT back to the gas payer. @@ -1508,3 +1523,22 @@ def _raffle_txn(self, multi_creds, txn_func, txn_args, txn_event) -> RaffleTxn: ) return {"txn_succeeded": txn_succeeded, "tx_receipt": tx_receipt} + + def _check_transfer_event(self, tx_receipt: Optional[TxReceipt]) -> bool: + """ + Check if transaction receipt has bulkTransfer event, to make sure that transaction was successful. + + Args: + tx_receipt (Optional[TxReceipt]): a dict with transaction receipt. + + Returns: + bool: returns True if transaction has bulkTransfer event, otherwise returns False. + """ + if not tx_receipt: + return False + + for log in tx_receipt.get("logs", {}): + for topic in log["topics"]: + if BULK_TRANSFER_EVENT in topic.hex(): + return True + return False diff --git a/package-lock.json b/package-lock.json index 77ab31b0..b3a7ecad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hmt-escrow", - "version": "0.14.6", + "version": "0.14.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4d600221..fa50573a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hmt-escrow", - "version": "0.14.6", + "version": "0.14.7", "description": "Launch escrow contracts to the HUMAN network", "main": "truffle.js", "directories": { diff --git a/setup.py b/setup.py index aee94bfb..15945758 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="hmt-escrow", - version="0.14.6", + version="0.14.7", author="HUMAN Protocol", description="A python library to launch escrow contracts to the HUMAN network.", url="https://github.com/humanprotocol/hmt-escrow", @@ -18,6 +18,7 @@ "boto3", "cryptography", "hmt-basemodels>=0.1.18", + "pysha3==1.0.2", "web3==5.24.0", ], ) diff --git a/test/hmt_escrow/test_job.py b/test/hmt_escrow/test_job.py index c8b3aaae..9418b4c9 100644 --- a/test/hmt_escrow/test_job.py +++ b/test/hmt_escrow/test_job.py @@ -225,8 +225,8 @@ def test_job_bulk_payout(self): ] self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) - def test_job_bulk_payout_with_encryption_option(self): - """Tests whether final results must be persisted in storage encrypted or plain.""" + def test_job_bulk_payout_with_false_encryption_option(self): + """Test that final results are stored encrypted""" job = Job(self.credentials, manifest) self.assertEqual(job.launch(self.rep_oracle_pub_key), True) self.assertEqual(job.setup(), True) @@ -253,10 +253,24 @@ def test_job_bulk_payout_with_encryption_option(self): encrypt_data=False, use_public_bucket=False, ) - mock_upload.reset_mock() - # Testing option as: encrypt final results: encrypt_final_results=True + def test_job_bulk_payout_with_true_encryption_option(self): + """Test that final results are stored uncrypted""" + job = Job(self.credentials, manifest) + self.assertEqual(job.launch(self.rep_oracle_pub_key), True) + self.assertEqual(job.setup(), True) + + payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))] + + final_results = {"results": 0} + + mock_upload = MagicMock(return_value=("hash", "url")) + + # Testing option as: encrypt final results: encrypt_final_results=True + with patch("hmt_escrow.job.upload") as mock_upload: # Bulk payout with final results as plain (not encrypted) + mock_upload.return_value = ("hash", "url") + job.bulk_payout( payouts=payouts, results=final_results, @@ -325,15 +339,19 @@ def test_job_bulk_payout_with_full_qualified_url(self): self.assertEqual(job.setup(), True) payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))] - final_results = {"results": 0} with patch( "hmt_escrow.job.handle_transaction_with_retry" - ) as transaction_retry_mock, patch("hmt_escrow.job.upload") as upload_mock: + ) as transaction_retry_mock, patch( + "hmt_escrow.job.upload" + ) as upload_mock, patch.object( + Job, "_check_transfer_event" + ) as _check_transfer_event_mock: key = "abcdefg" hash_ = f"s3{key}" upload_mock.return_value = hash_, key + _check_transfer_event_mock.return_value = True # Bulk payout with option to store final results privately job.bulk_payout( @@ -397,6 +415,10 @@ def test_retrieving_encrypted_final_results(self): self.assertEqual(persisted_final_results, final_results) # Bulk payout with encryption OFF + job = Job(self.credentials, manifest) + self.assertEqual(job.launch(self.rep_oracle_pub_key), True) + self.assertEqual(job.setup(), True) + job.bulk_payout( payouts=payouts, results=final_results,