Skip to content

Commit

Permalink
test: meta transaction account creation scenarios (#8568)
Browse files Browse the repository at this point in the history
Integration tests for different ways of creating accounts.

This adds some test utilities for implicit accounts, and it fixes the
meta tx checker functions to also work with implicit accounts, as
they don't use the account name as seed for the access key.
  • Loading branch information
jakmeier authored Feb 17, 2023
1 parent 6d77dac commit ba3bc8b
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 62 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ c2-chacha.workspace = true
curve25519-dalek.workspace = true
derive_more.workspace = true
ed25519-dalek.workspace = true
hex.workspace = true
near-account-id = { path = "../account-id" }
once_cell.workspace = true
primitive-types.workspace = true
Expand Down
14 changes: 14 additions & 0 deletions core/crypto/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use borsh::BorshDeserialize;
use secp256k1::rand::SeedableRng;

use crate::signature::{ED25519PublicKey, ED25519SecretKey, KeyType, PublicKey, SecretKey};
Expand Down Expand Up @@ -33,6 +34,19 @@ impl PublicKey {
_ => unimplemented!(),
}
}

pub fn from_implicit_account(account_id: &AccountId) -> Self {
assert!(account_id.is_implicit());
let mut public_key_data = Vec::with_capacity(33);
public_key_data.push(KeyType::ED25519 as u8);
public_key_data.extend(
hex::decode(account_id.as_ref().as_bytes())
.expect("account id was a valid hex of length 64 resulting in 32 bytes"),
);
assert_eq!(public_key_data.len(), 33);
PublicKey::try_from_slice(&public_key_data)
.expect("we should be able to deserialize ED25519 public key")
}
}

impl SecretKey {
Expand Down
3 changes: 3 additions & 0 deletions core/primitives/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,9 @@ pub enum ActionErrorKind {
/// Error occurs when a `CreateAccount` action is called on hex-characters
/// account of length 64. See implicit account creation NEP:
/// <https://github.com/nearprotocol/NEPs/pull/71>.
///
/// TODO(#8598): This error is named very poorly. A better name would be
/// `OnlyNamedAccountCreationAllowed`.
OnlyImplicitAccountCreationAllowed { account_id: AccountId },
/// Delete account whose state is large is temporarily banned.
DeleteAccountWithLargeState { account_id: AccountId },
Expand Down
21 changes: 19 additions & 2 deletions core/primitives/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;

use near_crypto::{EmptySigner, InMemorySigner, KeyType, PublicKey, Signature, Signer};
use near_crypto::{EmptySigner, InMemorySigner, KeyType, PublicKey, SecretKey, Signature, Signer};
use near_primitives_core::types::ProtocolVersion;

use crate::account::{AccessKey, AccessKeyPermission, Account};
Expand Down Expand Up @@ -491,9 +491,26 @@ pub fn create_test_signer(account_name: &str) -> InMemoryValidatorSigner {

/// Helper function that creates a new signer for a given account, that uses the account name as seed.
///
/// This also works for predefined implicit accounts, where the signer will use the implicit key.
///
/// Should be used only in tests.
pub fn create_user_test_signer(account_name: &str) -> InMemorySigner {
InMemorySigner::from_seed(account_name.parse().unwrap(), KeyType::ED25519, account_name)
let account_id = account_name.parse().unwrap();
if account_id == implicit_test_account() {
InMemorySigner::from_secret_key(account_id, implicit_test_account_secret())
} else {
InMemorySigner::from_seed(account_id, KeyType::ED25519, account_name)
}
}

/// A fixed implicit account for which tests can know the private key.
pub fn implicit_test_account() -> AccountId {
"061b1dd17603213b00e1a1e53ba060ad427cef4887bd34a5e0ef09010af23b0a".parse().unwrap()
}

/// Private key for the fixed implicit test account.
pub fn implicit_test_account_secret() -> SecretKey {
"ed25519:5roj6k68kvZu3UEJFyXSfjdKGrodgZUfFLZFpzYXWtESNsLWhYrq3JGi4YpqeVKuw1m9R2TEHjfgWT1fjUqB1DNy".parse().unwrap()
}

impl FinalExecutionOutcomeView {
Expand Down
180 changes: 166 additions & 14 deletions integration-tests/src/tests/client/features/delegate_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ use near_client::test_utils::TestEnv;
use near_crypto::{KeyType, PublicKey};
use near_primitives::account::AccessKey;
use near_primitives::config::ActionCosts;
use near_primitives::errors::{ActionsValidationError, InvalidTxError, TxExecutionError};
use near_primitives::test_utils::create_user_test_signer;
use near_primitives::errors::{
ActionError, ActionErrorKind, ActionsValidationError, InvalidTxError, TxExecutionError,
};
use near_primitives::test_utils::{create_user_test_signer, implicit_test_account};
use near_primitives::transaction::{
Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction,
FunctionCallAction, StakeAction, TransferAction,
Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction,
DeployContractAction, FunctionCallAction, StakeAction, TransferAction,
};
use near_primitives::types::{AccountId, Balance};
use near_primitives::version::{ProtocolFeature, ProtocolVersion};
use near_primitives::views::{
AccessKeyPermissionView, FinalExecutionOutcomeView, FinalExecutionStatus,
};
use near_test_contracts::smallest_rs_contract;
use near_test_contracts::{ft_contract, smallest_rs_contract};
use nearcore::config::GenesisExt;
use nearcore::NEAR_BASE;
use testlib::runtime_utils::{
Expand Down Expand Up @@ -117,15 +119,17 @@ fn check_meta_tx_execution(

let sender_before = node_user.view_balance(&sender).unwrap();
let relayer_before = node_user.view_balance(&relayer).unwrap();
let receiver_before = node_user.view_balance(&receiver).unwrap();
let receiver_before = node_user.view_balance(&receiver).unwrap_or(0);
let relayer_nonce_before = node_user
.get_access_key(&relayer, &PublicKey::from_seed(KeyType::ED25519, &relayer))
.unwrap()
.nonce;
let user_nonce_before = node_user
.get_access_key(&sender, &PublicKey::from_seed(KeyType::ED25519, &sender))
.unwrap()
.nonce;
let user_pubk = if sender.is_implicit() {
PublicKey::from_implicit_account(&sender)
} else {
PublicKey::from_seed(KeyType::ED25519, &sender)
};
let user_nonce_before = node_user.get_access_key(&sender, &user_pubk).unwrap().nonce;

let tx_result = node_user
.meta_tx(sender.clone(), receiver.clone(), relayer.clone(), actions.clone())
Expand Down Expand Up @@ -171,7 +175,7 @@ fn check_meta_tx_no_fn_call(
receiver: AccountId,
) -> FinalExecutionOutcomeView {
let fee_helper = fee_helper(node);
let gas_cost = normal_tx_cost + fee_helper.meta_tx_overhead_cost(&actions);
let gas_cost = normal_tx_cost + fee_helper.meta_tx_overhead_cost(&actions, &receiver);

let (tx_result, sender_diff, relayer_diff, receiver_diff) =
check_meta_tx_execution(node, actions, sender, relayer, receiver);
Expand Down Expand Up @@ -203,7 +207,7 @@ fn check_meta_tx_fn_call(
) -> FinalExecutionOutcomeView {
let fee_helper = fee_helper(node);
let num_fn_calls = actions.len();
let meta_tx_overhead_cost = fee_helper.meta_tx_overhead_cost(&actions);
let meta_tx_overhead_cost = fee_helper.meta_tx_overhead_cost(&actions, &receiver);

let (tx_result, sender_diff, relayer_diff, receiver_diff) =
check_meta_tx_execution(node, actions, sender, relayer, receiver);
Expand Down Expand Up @@ -401,8 +405,8 @@ fn meta_tx_delete_account() {
vec![Action::DeleteAccount(DeleteAccountAction { beneficiary_id: relayer.clone() })];

// special case balance check for deleting account
let gas_cost =
fee_helper.prepaid_delete_account_cost() + fee_helper.meta_tx_overhead_cost(&actions);
let gas_cost = fee_helper.prepaid_delete_account_cost()
+ fee_helper.meta_tx_overhead_cost(&actions, &receiver);
let (_tx_result, sender_diff, relayer_diff, receiver_diff) =
check_meta_tx_execution(&node, actions, sender, relayer, receiver.clone());

Expand Down Expand Up @@ -572,3 +576,151 @@ fn assert_ft_balance(
let balance = std::str::from_utf8(&response.result).expect("invalid UTF8");
assert_eq!(format!("\"{expected_balance}\""), balance);
}

/// Test account creation scenarios with meta transactions.
///
/// Named accounts aren't the primary use case for meta transactions but still
/// worth a test case.
#[test]
fn meta_tx_create_named_account() {
let relayer = bob_account();
let sender = alice_account();
let new_account = eve_dot_alice_account();
let node = RuntimeNode::new(&relayer);

let fee_helper = fee_helper(&node);
let amount = NEAR_BASE;

let public_key = PublicKey::from_seed(KeyType::ED25519, &new_account);

// That's the minimum to create a (useful) account.
let actions = vec![
Action::CreateAccount(CreateAccountAction {}),
Action::Transfer(TransferAction { deposit: amount }),
Action::AddKey(AddKeyAction { public_key, access_key: AccessKey::full_access() }),
];

// Check the account doesn't exist, yet. We want to create it.
node.view_account(&new_account).expect_err("account already exists");

let tx_cost = fee_helper.create_account_transfer_full_key_cost();
check_meta_tx_no_fn_call(&node, actions, tx_cost, amount, sender, relayer, new_account.clone());

// Check the account exists after we created it.
node.view_account(&new_account).expect("failed looking up account");
}

/// Try creating an implicit account with `CreateAction` which is not allowed in
/// or outside meta transactions and must fail with `OnlyImplicitAccountCreationAllowed`.
#[test]
fn meta_tx_create_implicit_account_fails() {
let relayer = bob_account();
let sender = alice_account();
let new_account: AccountId = implicit_test_account();
let node = RuntimeNode::new(&relayer);

let actions = vec![Action::CreateAccount(CreateAccountAction {})];
let tx_result = node
.user()
.meta_tx(sender.clone(), new_account.clone(), relayer.clone(), actions.clone())
.unwrap();

let account_creation_result = &tx_result.receipts_outcome[1].outcome.status;
assert!(matches!(
account_creation_result,
near_primitives::views::ExecutionStatusView::Failure(TxExecutionError::ActionError(
ActionError { kind: ActionErrorKind::OnlyImplicitAccountCreationAllowed { .. }, .. }
)),
));
}

/// Try creating an implicit account with a meta tx transfer and use the account
/// in the same meta transaction.
///
/// This is expected to fail with `AccountDoesNotExist`, known limitation of NEP-366.
/// It only works with accounts that already exists because it needs to do a
/// nonce check against the access key, which can only exist if the account exists.
#[test]
fn meta_tx_create_and_use_implicit_account() {
let relayer = bob_account();
let sender = alice_account();
let new_account: AccountId = implicit_test_account();
let node = RuntimeNode::new(&relayer);

// Check the account doesn't exist, yet. We will attempt creating it.
node.view_account(&new_account).expect_err("account already exists");

let initial_amount = nearcore::NEAR_BASE;
let actions = vec![
Action::Transfer(TransferAction { deposit: initial_amount }),
Action::DeployContract(DeployContractAction { code: ft_contract().to_vec() }),
];

// Execute and expect `AccountDoesNotExist`, as we try to call a meta
// transaction on a user that doesn't exist yet.
let tx_result =
node.user().meta_tx(sender.clone(), new_account.clone(), relayer.clone(), actions).unwrap();
let status = &tx_result.receipts_outcome[1].outcome.status;
assert!(matches!(
status,
near_primitives::views::ExecutionStatusView::Failure(TxExecutionError::ActionError(
ActionError { kind: ActionErrorKind::AccountDoesNotExist { account_id }, .. }
)) if *account_id == new_account,
));
}

/// Creating an implicit account with a meta tx transfer and use the account in
/// a second meta transaction.
///
/// Creation through a meta tx should work as normal, it's just that the relayer
/// pays for the storage and the user could delete the account and cash in,
/// hence this workflow is not ideal from all circumstances.
#[test]
fn meta_tx_create_implicit_account() {
let relayer = bob_account();
let sender = alice_account();
let new_account: AccountId = implicit_test_account();
let node = RuntimeNode::new(&relayer);

// Check account doesn't exist, yet
node.view_account(&new_account).expect_err("account already exists");

let fee_helper = fee_helper(&node);
let initial_amount = nearcore::NEAR_BASE;
let actions = vec![Action::Transfer(TransferAction { deposit: initial_amount })];
let tx_cost = fee_helper.create_account_transfer_full_key_cost();
check_meta_tx_no_fn_call(
&node,
actions,
tx_cost,
initial_amount,
sender.clone(),
relayer.clone(),
new_account.clone(),
);

// Check account exists with expected balance
node.view_account(&new_account).expect("failed looking up account");
let balance = node.view_balance(&new_account).expect("failed looking up balance");
assert_eq!(balance, initial_amount);

// Now test we can use this account in a meta transaction that sends back half the tokens to alice.
let transfer_amount = initial_amount / 2;
let actions = vec![Action::Transfer(TransferAction { deposit: transfer_amount })];
let tx_cost = fee_helper.transfer_cost();
check_meta_tx_no_fn_call(
&node,
actions,
tx_cost,
transfer_amount,
new_account.clone(),
relayer,
sender,
)
.assert_success();

// balance of the new account should NOT change, the relayer pays for it!
// (note: relayer balance checks etc are done in the shared checker function)
let balance = node.view_balance(&new_account).expect("failed looking up balance");
assert_eq!(balance, initial_amount);
}
1 change: 1 addition & 0 deletions test-utils/testlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ near-chain = { path = "../../chain/chain" }
near-crypto = { path = "../../core/crypto" }
near-primitives = { path = "../../core/primitives" }
near-test-contracts = { path = "../../runtime/near-test-contracts" }
node-runtime = { path = "../../runtime/runtime" }

[features]
default = []
Expand Down
Loading

0 comments on commit ba3bc8b

Please sign in to comment.