Skip to content

Commit

Permalink
Added pytest support for meta transactions (and a new sanity test) (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
mm-near authored Jan 20, 2023
1 parent a92fc5d commit 44ad2d3
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 2 deletions.
3 changes: 3 additions & 0 deletions nightly/pytest-sanity.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions pytest/lib/messages/tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ class DeleteAccount:
pass


class SignedDelegate:
pass


class DelegateAction:
pass


class Receipt:
pass

Expand Down Expand Up @@ -96,6 +104,7 @@ class DataReceiver:
['addKey', AddKey],
['deleteKey', DeleteKey],
['deleteAccount', DeleteAccount],
['delegate', SignedDelegate],
]
}
],
Expand All @@ -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']]
Expand Down
11 changes: 9 additions & 2 deletions pytest/lib/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions pytest/lib/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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],
Expand Down
94 changes: 94 additions & 0 deletions pytest/tests/sanity/meta_tx.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 44ad2d3

Please sign in to comment.