diff --git a/src/sakia/data/entities/transaction.py b/src/sakia/data/entities/transaction.py index 82096a04466c1ac12440460043ecc617962545f3..460ca51bf8641d6c5a01892ff706c5d738a7ed37 100644 --- a/src/sakia/data/entities/transaction.py +++ b/src/sakia/data/entities/transaction.py @@ -111,18 +111,21 @@ class Transaction: Transaction entity :param str currency: the currency of the transaction + :param str pubkey: the pubkey of the issuer :param str sha_hash: the hash of the transaction :param int written_block: the number of the block :param duniterpy.documents.BlockUID blockstamp: the blockstamp of the transaction :param int timestamp: the timestamp of the transaction - :param str signature: the signature - :param str issuer: the pubkey of the issuer - :param str receiver: the pubkey of the receiver + :param str signatures: the signature + :param tuple issuers: tuple of pubkey of the issuers + :param tuple receivers: tuple of pubkey of the receivers :param int amount: the amount :param int amount_base: the amount base :param str comment: a comment - :param str txid: the transaction id to sort transctions + :param int txid: the transaction id to sort transctions :param int state: the state of the transaction + :param bool local: is the transaction local + :param str raw: the raw string of the transaction """ TO_SEND = 0 diff --git a/src/sakia/data/processors/transactions.py b/src/sakia/data/processors/transactions.py index 04ff3822e76f2bb36eaad9510b21d0b0bad9396d..ce3d357892aa5df462f5f061109f004d00f0f0ab 100644 --- a/src/sakia/data/processors/transactions.py +++ b/src/sakia/data/processors/transactions.py @@ -92,7 +92,7 @@ class TransactionsProcessor: except sqlite3.IntegrityError: self._repo.update(tx) - def find_by_hash(self, pubkey, sha_hash): + def find_by_hash(self, pubkey: str, sha_hash: str) -> Transaction: return self._repo.get_one(pubkey=pubkey, sha_hash=sha_hash) def awaiting(self, currency): diff --git a/src/sakia/data/repositories/transactions.py b/src/sakia/data/repositories/transactions.py index 04ba645b34d3f201029be53a3bd296923c224d18..c51c8a5d35a3f3d07b2ec16e49ca1ba4d404cdbc 100644 --- a/src/sakia/data/repositories/transactions.py +++ b/src/sakia/data/repositories/transactions.py @@ -75,7 +75,7 @@ class TransactionsRepo: def get_one(self, **search): """ Get an existing transaction in the database - :param dict search: the criterions of the lookup + :param ** search: the criterions of the lookup :rtype: sakia.data.entities.Transaction """ filters = [] diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index 3f0a548ea6cc0fbdee4db78ba79b6f635c77efdd..e280500532fcf37342fbe42b1ef2025d8fe008f8 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -1,3 +1,5 @@ +from hashlib import sha256 + import jsonschema import attr import logging @@ -18,7 +20,7 @@ from duniterpy.documents import ( from duniterpy.documents import Identity as IdentityDoc from duniterpy.documents import Transaction as TransactionDoc from duniterpy.documents.transaction import reduce_base -from duniterpy.grammars.output import Condition, Operator, SIG, CSV +from duniterpy.grammars.output import Condition, Operator, SIG, CSV, CLTV, XHX from duniterpy.api import bma from sakia.data.entities import Identity, Transaction, Source from sakia.data.processors import ( @@ -298,13 +300,13 @@ class DocumentsService: return document.signed_raw(), identity - def tx_sources(self, amount, amount_base, currency, pubkey): + def tx_sources(self, amount, amount_base, currency, key: SigningKey): """ Get inputs to generate a transaction with a given amount of money :param int amount: The amount target value :param int amount_base: The amount base target value :param str currency: The community target of the transaction - :param str pubkey: The pubkey owning the sources + :param str key: The key owning the sources :return: The list of inputs to use in the transaction document """ @@ -321,7 +323,7 @@ class DocumentsService: return i amount, amount_base = reduce_base(amount, amount_base) - available_sources = self._sources_processor.available(currency, pubkey) + available_sources = self._sources_processor.available(currency, key.pubkey) if available_sources: current_base = max([src.base for src in available_sources]) value = 0 @@ -331,6 +333,12 @@ class DocumentsService: buf_sources = list(available_sources) while current_base >= 0: for s in [src for src in available_sources if src.base == current_base]: + condition = pypeg2.parse(s.conditions, Condition) + # evaluate the condition + if not self.evaluate_condition( + currency, condition, [key], [], s.identifier + ): + continue test_sources = sources + [s] val = current_value(test_sources, overheads) # if we have to compute an overhead @@ -503,7 +511,7 @@ class DocumentsService: forged_tx = [] sources = [None] * 41 while len(sources) > 40: - result = self.tx_sources(int(amount), amount_base, currency, key.pubkey) + result = self.tx_sources(int(amount), amount_base, currency, key) sources = result[0] computed_outputs = result[1] overheads = result[2] @@ -626,3 +634,97 @@ class DocumentsService: return result except NotEnoughChangeError as e: return (False, str(e)), tx_entities + + def evaluate_condition( + self, + currency, + condition: Condition, + keys: list, + passwords, + identifier: str, + result: bool = True, + ) -> bool: + """ + Evaluate a source lock condition + Support multiple signatures and passwords + + :param passwords: + :param str currency: Name of currency + :param Condition condition: Condition instance + :param [SigningKey] keys: Keys to unlock condition (first key is the source owner key) + :param str identifier: Source transaction identifier + :param bool result: result accumulator + :return: + """ + left = False + right = False + # if left param is a condition... + if isinstance(condition.left, Condition): + # evaluate condition + left = self.evaluate_condition( + currency, condition.left, keys, passwords, identifier, result + ) + # if right param is a condition... + if isinstance(condition.right, Condition): + # evaluate condition + right = self.evaluate_condition( + currency, condition.right, keys, passwords, identifier, result + ) + # if left param is a SIG... + if isinstance(condition.left, SIG) and condition.left.pubkey in ( + key.pubkey for key in keys + ): + left = True + # if left param is a CSV value... + if isinstance(condition.left, CSV): + # capture transaction of the source + tx = self._transactions_processor.find_by_hash(keys[0].pubkey, identifier) + if tx: + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if tx time + CSV delay <= blockchain time + left = tx.timestamp + int(condition.left.time) <= median_time + # if left param is a CLTV value... + if isinstance(condition.left, CLTV): + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if CL:TV value <= blockchain time + left = int(condition.left.timestamp) <= median_time + # if left param is a XHX value... + if isinstance(condition.left, XHX): + left = condition.left.sha_hash in [ + sha256(password).hexdigest().upper() for password in passwords + ] + # if no op then stop evaluation... + if not condition.op: + return left + # if right param is a SIG... + if isinstance(condition.right, SIG) and condition.right.pubkey in ( + key.pubkey for key in keys + ): + right = True + # if right param is a CSV value... + if isinstance(condition.right, CSV): + # capture transaction of the source + tx = self._transactions_processor.find_by_hash(keys[0].pubkey, identifier) + if tx: + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if tx time + CSV delay <= blockchain time + right = tx.timestamp + int(condition.right.time) <= median_time + # if right param is a CLTV value... + if isinstance(condition.right, CLTV): + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if CLTV value <= blockchain time + right = int(condition.right.timestamp) <= median_time + # if right param is a XHX value... + if isinstance(condition.right, XHX): + right = condition.right.sha_hash in [ + sha256(password).upper() for password in passwords + ] + # if operator AND... + if condition.op == "&&": + return left & right + # operator OR + return left | right diff --git a/tests/unit/services/test_documents.py b/tests/unit/services/test_documents.py index 00084fa94b0ad7d2ce2106ba88303aff979742ec..4374dca2d48041ecf1398ea2a248008fbdd976ba 100644 --- a/tests/unit/services/test_documents.py +++ b/tests/unit/services/test_documents.py @@ -1,4 +1,12 @@ +from hashlib import sha256 + +import pypeg2 import pytest +from duniterpy.grammars import output +from duniterpy.grammars.output import Condition + +from sakia.data.entities import Transaction, Source +from sakia.data.repositories import TransactionsRepo, SourcesRepo @pytest.mark.asyncio @@ -69,3 +77,619 @@ async def test_lock_mode_1(application_with_one_connection, fake_server, bob, al "Outputs:\n100:0:SIG(F3HWkYnUSbdpEueosKqzYd1m8ftwojwE2uXR7ScoAVKo) || (SIG(GfFUvqaVSgCt6nFDQCAuULWk6K16MUDckeyBJQFcaYj7) && CSV(604800))\n" in sakia_tx_list[0].raw ) + + +def test_evaluate_condition_source_lock_mode_0( + application_with_one_connection, bob, alice, meta_repo +): + application_with_one_connection.instanciate_services() + + # capture blockchain median time + median_time = application_with_one_connection.blockchain_service._blockchain_processor.time( + application_with_one_connection.currency + ) + tx_hash = "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365" + + # add a transaction in bob tx history + transactions_repo = TransactionsRepo(meta_repo.conn) + transactions_repo.insert( + Transaction( + "testcurrency", + bob.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + transactions_repo.insert( + Transaction( + "testcurrency", + alice.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + # test simple signature condition + condition = output.Condition.token(output.SIG.token(bob.key.pubkey),) + # bob can spend this source + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, condition, [bob.key], [], tx_hash + ) + is True + ) + # alice can not + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + condition, + [alice.key], + [], + tx_hash, + ) + is False + ) + + +def test_evaluate_condition_source_lock_mode_1( + application_with_one_connection, bob, alice, meta_repo +): + application_with_one_connection.instanciate_services() + + # capture blockchain median time + median_time = application_with_one_connection.blockchain_service._blockchain_processor.time( + application_with_one_connection.currency + ) + tx_hash = "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365" + + # add a transaction in bob tx history + transactions_repo = TransactionsRepo(meta_repo.conn) + transactions_repo.insert( + Transaction( + "testcurrency", + bob.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + transactions_repo.insert( + Transaction( + "testcurrency", + alice.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + + # test condition: receiver or (issuer and CSV(one week)) + condition = output.Condition.token( + output.SIG.token(bob.key.pubkey), + output.Operator.token("||"), + output.Condition.token( + output.SIG.token(alice.key.pubkey), + output.Operator.token("&&"), + output.CSV.token(604800), + ), + ) + # bob try to spend his source + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, condition, [bob.key], [], tx_hash + ) + is True + ) + # alice try to get back this source before the CSV delay + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + condition, + [alice.key], + [], + tx_hash, + ) + is False + ) + + # Transaction was made one week ago... + transactions_repo.update( + Transaction( + "testcurrency", + bob.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time - 604800, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + transactions_repo.update( + Transaction( + "testcurrency", + alice.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time - 604800, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + tx = transactions_repo.get_one(sha_hash=tx_hash) + assert tx.timestamp == median_time - 604800 + + # bob try to spend his source + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, condition, [bob.key], [], tx_hash + ) + is True + ) + # alice can get back this source after the CSV delay + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + condition, + [alice.key], + [], + tx_hash, + ) + is True + ) + + +def test_evaluate_condition_source_multisig( + application_with_one_connection, bob, alice, meta_repo +): + application_with_one_connection.instanciate_services() + + # capture blockchain median time + median_time = application_with_one_connection.blockchain_service._blockchain_processor.time( + application_with_one_connection.currency + ) + tx_hash = "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365" + + # add a transaction in bob tx history + transactions_repo = TransactionsRepo(meta_repo.conn) + transactions_repo.insert( + Transaction( + "testcurrency", + bob.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + transactions_repo.insert( + Transaction( + "testcurrency", + alice.key.pubkey, + tx_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, + bob.key.pubkey, + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + + # test condition: multi signatures + condition = output.Condition.token( + output.SIG.token(bob.key.pubkey), + output.Operator.token("&&"), + output.SIG.token(alice.key.pubkey), + ) + # bob can not spend this source alone + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, condition, [bob.key], [], tx_hash + ) + is False + ) + # alice can not spend this source alone + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + condition, + [alice.key], + [], + tx_hash, + ) + is False + ) + # alice && bob together only can spend this source + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + condition, + [alice.key, bob.key], + [], + tx_hash, + ) + is True + ) + + +def test_evaluate_condition_source_atomic_swap( + application_with_one_connection, bob, alice, meta_repo +): + application_with_one_connection.instanciate_services() + transactions_repo = TransactionsRepo(meta_repo.conn) + + # capture blockchain median time + median_time = application_with_one_connection.blockchain_service._blockchain_processor.time( + application_with_one_connection.currency + ) + + tx1_hash = "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365" + # add TX1 transaction in alice tx history + transactions_repo.insert( + Transaction( + "testcurrency", + alice.key.pubkey, # alice history + tx1_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + # add TX1 transaction in bob tx history + transactions_repo.insert( + Transaction( + "testcurrency", + bob.key.pubkey, # bob history + tx1_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + + # atomic swap initiator TX1 condition created by alice + # (XHX(password) && SIG(bob)) || (SIG(alice) && SIG(bob)) || (SIG(alice) && CSV(48h)) + password = "test".encode("utf8") # password must be encoded in str not unicode + hash_password = sha256(password).hexdigest().upper() + tx1_condition = output.Condition.token( + output.Condition.token( + output.Condition.token( + output.XHX.token(hash_password), + output.Operator.token("&&"), + output.SIG.token(bob.key.pubkey), + ), + output.Operator.token("||"), + output.Condition.token( + output.SIG.token(alice.key.pubkey), + output.Operator.token("&&"), + output.SIG.token(bob.key.pubkey), + ), + ), + output.Operator.token("||"), + output.Condition.token( + output.SIG.token(alice.key.pubkey), + output.Operator.token("&&"), + output.CSV.token(172800), + ), + ) + + tx2_hash = "ACAB5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365" + # add TX2 transaction in alice tx history + transactions_repo.insert( + Transaction( + "testcurrency", + alice.key.pubkey, # alice history + tx2_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + # add TX2 transaction in bob tx history + transactions_repo.insert( + Transaction( + "testcurrency", + bob.key.pubkey, # bob history + tx2_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + + # test atomic swap participant TX2 condition created by bob with alice password hash + # (XHX(password) && SIG(alice)) || (SIG(alice) && SIG(bob)) || (SIG(bob) && CSV(24h)) + tx2_condition = output.Condition.token( + output.Condition.token( + output.Condition.token( + output.XHX.token(hash_password), + output.Operator.token("&&"), + output.SIG.token(alice.key.pubkey), + ), + output.Operator.token("||"), + output.Condition.token( + output.SIG.token(alice.key.pubkey), + output.Operator.token("&&"), + output.SIG.token(bob.key.pubkey), + ), + ), + output.Operator.token("||"), + output.Condition.token( + output.SIG.token(bob.key.pubkey), + output.Operator.token("&&"), + output.CSV.token(86400), + ), + ) + + # alice spend the source from tx2 with the password + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx2_condition, + [alice.key], + [password], + tx2_hash, + ) + is True + ) + + # the password is revealed in the unlock of the tx3 spending tx2 + # bob can now spend tx1 using the password + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx1_condition, + [bob.key], + [password], + tx1_hash, + ) + is True + ) + + # alice and bob can sign together to spend tx1 + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx1_condition, + [bob.key, alice.key], + [password], + tx1_hash, + ) + is True + ) + + # alice and bob can sign together to spend tx2 + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx2_condition, + [bob.key, alice.key], + [password], + tx2_hash, + ) + is True + ) + + # alice can not spend the source from tx2 without the password + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx2_condition, + [alice.key], + [], + tx2_hash, + ) + is False + ) + + # bob can not spend tx1 without the password + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx1_condition, + [bob.key], + [], + tx2_hash, + ) + is False + ) + + # TX1 Transaction was made 48h ago... + # update TX1 transaction in alice tx history + transactions_repo.update( + Transaction( + "testcurrency", + alice.key.pubkey, # alice history + tx1_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time - 172800, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + # update TX1 transaction in bob tx history + transactions_repo.update( + Transaction( + "testcurrency", + bob.key.pubkey, # bob history + tx1_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time - 172800, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + + # TX2 Transaction was made 24h ago... + # update TX2 transaction in alice tx history + transactions_repo.update( + Transaction( + "testcurrency", + alice.key.pubkey, # alice history + tx2_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time - 86400, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + # update TX2 transaction in bob tx history + transactions_repo.update( + Transaction( + "testcurrency", + bob.key.pubkey, # bob history + tx2_hash, + 20, + "15-76543400E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + median_time - 86400, + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + alice.key.pubkey, # do not care + bob.key.pubkey, # do not care + 1565, + 1, + "", + 0, + Transaction.VALIDATED, + ) + ) + + # alice can get back the source from tx1 without the password after 48h + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx1_condition, + [alice.key], + [], + tx1_hash, + ) + is True + ) + + # bob can spend tx2 without the password after 24h + assert ( + application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.currency, + tx2_condition, + [bob.key], + [], + tx2_hash, + ) + is True + )