From 44ad2d3745e81085403efd250c7c59d5342ccd98 Mon Sep 17 00:00:00 2001 From: mm-near <91919554+mm-near@users.noreply.github.com> Date: Fri, 20 Jan 2023 16:00:10 +0100 Subject: [PATCH] Added pytest support for meta transactions (and a new sanity test) (#8394) This PR adds a pytest support for meta transactions. It also adds a new sanity check, that executes an 'add-key' action using meta transactions. --- nightly/pytest-sanity.txt | 3 ++ pytest/lib/messages/tx.py | 26 ++++++++++ pytest/lib/serializer.py | 11 +++- pytest/lib/transaction.py | 50 ++++++++++++++++++ pytest/tests/sanity/meta_tx.py | 94 ++++++++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 pytest/tests/sanity/meta_tx.py diff --git a/nightly/pytest-sanity.txt b/nightly/pytest-sanity.txt index 7cd9c8f1f93..7ab05afbb9b 100644 --- a/nightly/pytest-sanity.txt +++ b/nightly/pytest-sanity.txt @@ -139,3 +139,6 @@ pytest --skip-build --timeout=1h sanity/docker.py pytest sanity/recompress_storage.py pytest sanity/recompress_storage.py --features nightly + +# This is the test for meta transactions. +pytest sanity/meta_tx.py --features nightly \ No newline at end of file diff --git a/pytest/lib/messages/tx.py b/pytest/lib/messages/tx.py index 419efa7e2cf..214e4fedeab 100644 --- a/pytest/lib/messages/tx.py +++ b/pytest/lib/messages/tx.py @@ -45,6 +45,14 @@ class DeleteAccount: pass +class SignedDelegate: + pass + + +class DelegateAction: + pass + + class Receipt: pass @@ -96,6 +104,7 @@ class DataReceiver: ['addKey', AddKey], ['deleteKey', DeleteKey], ['deleteAccount', DeleteAccount], + ['delegate', SignedDelegate], ] } ], @@ -115,6 +124,23 @@ class DataReceiver: ['gas', 'u64'], ['deposit', 'u128']] } ], + [ + SignedDelegate, { + 'kind': + 'struct', + 'fields': [['delegateAction', DelegateAction], + ['signature', Signature]] + } + ], + [ + DelegateAction, { + 'kind': + 'struct', + 'fields': [['senderId', 'string'], ['receiverId', 'string'], + ['actions', [Action]], ['nonce', 'u64'], + ['maxBlockHeight', 'u64'], ['publicKey', PublicKey]] + } + ], [Transfer, { 'kind': 'struct', 'fields': [['deposit', 'u128']] diff --git a/pytest/lib/serializer.py b/pytest/lib/serializer.py index 692f47dae07..60808ba0cb1 100644 --- a/pytest/lib/serializer.py +++ b/pytest/lib/serializer.py @@ -115,14 +115,21 @@ def serialize_struct(self, obj): structSchema = self.schema[type(obj)] if structSchema['kind'] == 'struct': for fieldName, fieldType in structSchema['fields']: - self.serialize_field(getattr(obj, fieldName), fieldType) + try: + self.serialize_field(getattr(obj, fieldName), fieldType) + except AssertionError as exc: + raise AssertionError(f"Error in field {fieldName}") from exc elif structSchema['kind'] == 'enum': name = getattr(obj, structSchema['field']) for idx, (fieldName, fieldType) in enumerate(structSchema['values']): if fieldName == name: self.serialize_num(idx, 1) - self.serialize_field(getattr(obj, fieldName), fieldType) + try: + self.serialize_field(getattr(obj, fieldName), fieldType) + except AssertionError as exc: + raise AssertionError( + f"Error in field {fieldName}") from exc break else: assert False, name diff --git a/pytest/lib/transaction.py b/pytest/lib/transaction.py index ec3de0b7865..023ee8c220c 100644 --- a/pytest/lib/transaction.py +++ b/pytest/lib/transaction.py @@ -43,6 +43,40 @@ def sign_and_serialize_transaction(receiverId, nonce, actions, blockHash, return BinarySerializer(schema).serialize(signedTx) +def compute_delegated_action_hash(senderId, receiverId, actions, nonce, + maxBlockHeight, publicKey): + delegateAction = DelegateAction() + delegateAction.senderId = senderId + delegateAction.receiverId = receiverId + delegateAction.actions = actions + delegateAction.nonce = nonce + delegateAction.maxBlockHeight = maxBlockHeight + delegateAction.publicKey = PublicKey() + delegateAction.publicKey.keyType = 0 + delegateAction.publicKey.data = publicKey + msg = BinarySerializer(schema).serialize(delegateAction) + hash_ = hashlib.sha256(msg).digest() + + return delegateAction, hash_ + + +# Used by meta-transactions. +# Creates a SignedDelegate that is later put into the DelegateAction by relayer. +def create_signed_delegated_action(senderId, receiverId, actions, nonce, + maxBlockHeight, publicKey, sk): + delegated_action, hash_ = compute_delegated_action_hash( + senderId, receiverId, actions, nonce, maxBlockHeight, publicKey) + + signature = Signature() + signature.keyType = 0 + signature.data = SigningKey(sk).sign(hash_) + + signedDA = SignedDelegate() + signedDA.delegateAction = delegated_action + signedDA.signature = signature + return signedDA + + def create_create_account_action(): createAccount = CreateAccount() action = Action() @@ -133,6 +167,22 @@ def create_delete_account_action(beneficiary): return action +def create_delegate_action(signedDelegate): + action = Action() + action.enum = 'delegate' + action.delegate = signedDelegate + return action + + +def sign_delegate_action(signedDelegate, signer_key, contract_id, nonce, + blockHash): + action = create_delegate_action(signedDelegate) + return sign_and_serialize_transaction(contract_id, nonce, [action], + blockHash, signer_key.account_id, + signer_key.decoded_pk(), + signer_key.decoded_sk()) + + def sign_create_account_tx(creator_key, new_account_id, nonce, block_hash): action = create_create_account_action() return sign_and_serialize_transaction(new_account_id, nonce, [action], diff --git a/pytest/tests/sanity/meta_tx.py b/pytest/tests/sanity/meta_tx.py new file mode 100644 index 00000000000..3e62af43a10 --- /dev/null +++ b/pytest/tests/sanity/meta_tx.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Tests the meta transaction flow. +# Creates a new account (candidate.test0) with a fixed amount of tokens. +# Afterwards, creates the meta transaction that adds a new key to this account, but the gas is paid by someone else (test0) account. +# At the end, verifies that key has been added succesfully and that the amount of tokens in candidate didn't change. + +import base58 +import pathlib +import sys +import typing + +import unittest + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / 'lib')) + +from cluster import start_cluster, LocalNode +import utils +import transaction +import key + + +class Nonce: + """ Helper class to manage nonces (automatically increase them when they are used. """ + + def __init__(self, current_nonce: int = 0): + self.nonce = current_nonce + + def use_nonce(self) -> int: + self.nonce += 1 + return self.nonce + + +def create_nonce_from_node(node: LocalNode, account_id: str, pk: str) -> Nonce: + nn = node.get_nonce_for_pk(account_id, pk) + assert nn, "Nonce missing for the candidate account" + return Nonce(nn) + + +# Returns the number of keys and current amount for a given account +def check_account_status(node: LocalNode, + account_id: str) -> typing.Tuple[int, int]: + current_keys = node.get_access_key_list(account_id)['result']['keys'] + account_state = node.get_account(account_id)['result'] + return (len(current_keys), int(account_state['amount'])) + + +class TestMetaTransactions(unittest.TestCase): + + def test_meta_tx(self): + nodes: list[LocalNode] = start_cluster(2, 0, 1, None, [], {}) + _, hash_ = utils.wait_for_blocks(nodes[0], target=10) + + node0_nonce = Nonce() + + CANDIDATE_ACCOUNT = "candidate.test0" + CANDIDATE_STARTING_AMOUNT = 123 * (10**24) + + # create new account + candidate_key = key.Key.from_random(CANDIDATE_ACCOUNT) + + tx = transaction.sign_create_account_with_full_access_key_and_balance_tx( + nodes[0].signer_key, candidate_key.account_id, candidate_key, + CANDIDATE_STARTING_AMOUNT, node0_nonce.use_nonce(), + base58.b58decode(hash_.encode('utf8'))) + nodes[0].send_tx_and_wait(tx, 100) + + self.assertEqual(check_account_status(nodes[0], CANDIDATE_ACCOUNT), + (1, CANDIDATE_STARTING_AMOUNT)) + + candidate_nonce = create_nonce_from_node(nodes[0], + candidate_key.account_id, + candidate_key.pk) + + # Now let's prepare the meta transaction. + new_key = key.Key.from_random("new_key") + add_new_key_tx = transaction.create_full_access_key_action( + new_key.decoded_pk()) + signed_meta_tx = transaction.create_signed_delegated_action( + CANDIDATE_ACCOUNT, CANDIDATE_ACCOUNT, [add_new_key_tx], + candidate_nonce.use_nonce(), 1000, candidate_key.decoded_pk(), + candidate_key.decoded_sk()) + + meta_tx = transaction.sign_delegate_action( + signed_meta_tx, nodes[0].signer_key, CANDIDATE_ACCOUNT, + node0_nonce.use_nonce(), base58.b58decode(hash_.encode('utf8'))) + + nodes[0].send_tx_and_wait(meta_tx, 100) + + self.assertEqual(check_account_status(nodes[0], CANDIDATE_ACCOUNT), + (2, CANDIDATE_STARTING_AMOUNT)) + + +if __name__ == '__main__': + unittest.main()