Skip to content

Commit

Permalink
feat(e2e): wallet restoration load test
Browse files Browse the repository at this point in the history
- use artillery as load test engine
- add wallet restoration scenario
- update readme with usage
  • Loading branch information
Ivaylo Andonov committed Nov 22, 2022
1 parent 3b7c820 commit 9be492d
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 222 deletions.
14 changes: 13 additions & 1 deletion packages/e2e/README.md
Expand Up @@ -288,8 +288,20 @@ $ yarn workspace @cardano-sdk/e2e test:web-extension
**Artillery stress tests** are not exactly _e2e tests_ in the meaning of _jest tests_; the SDK is run through artillery.
They are not aimed to check if specific functionalities work or not, but to stress test APIs.

Currently only one artillery test is implemented ad it stresses a stake pool search endpoint. To run it against the local-network endpoint
Currently a few artillery load test scenarios are implemented.
The main purpose is to simulate expected load against provider endpoints or wallet initialization/restoration.

**The Artillery tests** are currently configured to run only with 1 worker.
With plan to design and introduce our custom Distributed load test solution in post MVP stage.

To run stake pool search scenario against the local-network endpoint:

```bash
$ STAKE_POOL_PROVIDER_URL="http://localhost:4000/stake-pool" yarn artillery:stake-pool-query
```

To run wallet restoration scenario against the local-network endpoint:

```bash
$ ARRIVAL_PHASE_DURATION_IN_SECS={duration of how long virtual users will be generated} WALLETS_COUNT={wallets count to restore} yarn artillery:wallet-restoration
```
29 changes: 29 additions & 0 deletions packages/e2e/env/.env.artillery
@@ -0,0 +1,29 @@
# Logger
TL_LEVEL=info

# Providers setup
FAUCET_PROVIDER=cardano-wallet
FAUCET_PROVIDER_PARAMS='{"baseUrl":"http://localhost:8090/v2","mnemonic":"fire method repair aware foot tray accuse brother popular olive find account sick rocket next"}'
KEY_MANAGEMENT_PROVIDER=inMemory
KEY_MANAGEMENT_PARAMS='{"accountIndex": 0, "networkId": 0, "password":"some_password","mnemonic":"vacant violin soft weird deliver render brief always monitor general maid smart jelly core drastic erode echo there clump dizzy card filter option defense"}'
ASSET_PROVIDER=http
ASSET_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/asset"}'
CHAIN_HISTORY_PROVIDER=http
CHAIN_HISTORY_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/chain-history"}'
NETWORK_INFO_PROVIDER=http
NETWORK_INFO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/network-info"}'
REWARDS_PROVIDER=http
REWARDS_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/rewards"}'
TX_SUBMIT_PROVIDER=http
TX_SUBMIT_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/tx-submit"}'
UTXO_PROVIDER=http
UTXO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/utxo"}'
STAKE_POOL_PROVIDER=http
STAKE_POOL_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/stake-pool"}'

DB_SYNC_CONNECTION_STRING='postgresql://postgres:doNoUseThisSecret!@localhost:5432/cexplorer'
OGMIOS_SERVER_URL='ws://localhost:1340/'

# Wallet restoration load test scenario
ARRIVAL_PHASE_DURATION_IN_SECS=60
WALLETS_COUNT=20
2 changes: 2 additions & 0 deletions packages/e2e/package.json
Expand Up @@ -15,6 +15,7 @@
},
"scripts": {
"artillery:stake-pool-query": "WORKERS=1 node -r ts-node/register ../../node_modules/.bin/artillery run test/artillery/StakePoolSearch.yml",
"artillery:wallet-restoration": "WORKERS=1 node -r ts-node/register ../../node_modules/.bin/artillery run --dotenv ./.env test/artillery/wallet-restoration/WalletRestoration.yml",
"test": "shx echo 'test' command not implemented yet",
"test:wallet": "jest -c jest.config.js --forceExit --selectProjects wallet --runInBand --verbose",
"test:load-testing": "jest -c jest.config.js --selectProjects load-testing --runInBand --verbose",
Expand Down Expand Up @@ -88,6 +89,7 @@
"get-port-please": "^2.5.0",
"lodash": "^4.17.21",
"optionator": "^0.9.1",
"pg": "^8.7.3",
"rxjs": "^7.4.0",
"ts-log": "^2.2.4",
"ts-node": "^10.8.1",
Expand Down
12 changes: 8 additions & 4 deletions packages/e2e/src/factories.ts
Expand Up @@ -14,6 +14,7 @@ import {
AsyncKeyAgent,
CommunicationType,
InMemoryKeyAgent,
KeyAgent,
KeyAgentDependencies,
LedgerKeyAgent,
TrezorKeyAgent,
Expand Down Expand Up @@ -276,6 +277,7 @@ export type GetWalletProps = {
polling?: PollingConfig;
stores?: storage.WalletStores;
customKeyParams?: KeyAgentFactoryProps;
keyAgent?: KeyAgent;
};

/**
Expand Down Expand Up @@ -304,7 +306,7 @@ const patchInitializeTxToRespectEpochBoundary = <T extends ObservableWallet>(
* @returns an object containing the wallet and providers passed to it
*/
export const getWallet = async (props: GetWalletProps) => {
const { env, idx, logger, name, polling, stores, customKeyParams } = props;
const { env, idx, logger, name, polling, stores, customKeyParams, keyAgent } = props;
const providers = {
assetProvider: await assetProviderFactory.create(env.ASSET_PROVIDER, env.ASSET_PROVIDER_PARAMS, logger),
chainHistoryProvider: await chainHistoryProviderFactory.create(
Expand Down Expand Up @@ -334,9 +336,11 @@ export const getWallet = async (props: GetWalletProps) => {
const keyManagementParams = { ...envKeyParams, ...(idx === undefined ? {} : { accountIndex: idx }) };

const { wallet } = await setupWallet({
createKeyAgent: await keyManagementFactory.create(env.KEY_MANAGEMENT_PROVIDER, keyManagementParams, logger),
createWallet: async (keyAgent: AsyncKeyAgent) =>
new SingleAddressWallet({ name, polling }, { ...providers, keyAgent, logger, stores })
createKeyAgent: keyAgent
? () => Promise.resolve(util.createAsyncKeyAgent(keyAgent))
: await keyManagementFactory.create(env.KEY_MANAGEMENT_PROVIDER, keyManagementParams, logger),
createWallet: async (asyncKeyAgent: AsyncKeyAgent) =>
new SingleAddressWallet({ name, polling }, { ...providers, keyAgent: asyncKeyAgent, logger, stores })
});

const [{ address, rewardAccount }] = await firstValueFrom(wallet.addresses$);
Expand Down
1 change: 1 addition & 0 deletions packages/e2e/src/index.ts
@@ -1,2 +1,3 @@
export * from './factories';
export * from './FaucetProvider';
export * from './util/StubKeyAgent';
61 changes: 61 additions & 0 deletions packages/e2e/src/util/StubKeyAgent.ts
@@ -0,0 +1,61 @@
import {
AccountAddressDerivationPath,
AccountKeyDerivationPath,
GroupedAddress,
KeyAgent,
SerializableKeyAgentData,
SignBlobResult,
SignTransactionOptions
} from '@cardano-sdk/key-management';
import { Cardano, NotImplementedError } from '@cardano-sdk/core';

export class StubKeyAgent implements KeyAgent {
readonly #knownAddresses: GroupedAddress[];

constructor(groupedAddress: GroupedAddress) {
this.#knownAddresses = [groupedAddress];
}

get knownAddresses(): GroupedAddress[] {
return this.#knownAddresses;
}

get networkId(): Cardano.NetworkId {
throw new NotImplementedError('networkId');
}

get accountIndex(): number {
throw new NotImplementedError('accountIndex');
}

get serializableData(): SerializableKeyAgentData {
throw new NotImplementedError('serializableData');
}

get extendedAccountPublicKey(): Cardano.Bip32PublicKey {
throw new NotImplementedError('extendedAccountPublicKey');
}

deriveAddress(_derivationPath: AccountAddressDerivationPath): Promise<GroupedAddress> {
throw new NotImplementedError('deriveAddress');
}

derivePublicKey(_derivationPath: AccountKeyDerivationPath): Promise<Cardano.Ed25519PublicKey> {
throw new NotImplementedError('derivePublicKey');
}

signBlob(_derivationPath: AccountKeyDerivationPath, _blob: Cardano.util.HexBlob): Promise<SignBlobResult> {
throw new NotImplementedError('signBlob');
}

signTransaction(
_txInternals: Cardano.TxBodyWithHash,
_options?: SignTransactionOptions | undefined
): Promise<Cardano.Signatures> {
throw new NotImplementedError('signTransaction');
}

exportRootPrivateKey(): Promise<Cardano.Bip32PrivateKey> {
throw new NotImplementedError('exportRootPrivateKey');
}
}
@@ -0,0 +1,84 @@
import { AddressType, GroupedAddress } from '@cardano-sdk/key-management';
import { AddressesModel, WalletVars } from './types';
import { Cardano } from '@cardano-sdk/core';
import { FunctionHook } from '../artillery';
import { Pool, QueryResult } from 'pg';
import { StubKeyAgent, getWallet } from '../../../src';
import { env } from '../../environment';
import { findAddressesWithRewardsHistory } from './queries';
import { logger } from '@cardano-sdk/util-dev';
import { walletReady } from '../../util';

const operationName = 'wallet-restoration';
const SHORTAGE_OF_WALLETS_FOUND_ERROR_MESSAGE = 'Addresses found from db are less than desired wallets count';

const mapToGroupedAddress = (addrModel: AddressesModel): GroupedAddress => ({
accountIndex: 0,
address: Cardano.Address(addrModel.address),
index: 0,
networkId: addrModel.address.startsWith('addr_test') ? Cardano.NetworkId.testnet : Cardano.NetworkId.mainnet,
rewardAccount: Cardano.RewardAccount(addrModel.stake_address),
type: AddressType.External
});

export const findAddresses: FunctionHook<WalletVars> = async ({ vars }, ee, done) => {
vars.walletsCount = Number(env.WALLETS_COUNT);
const db: Pool = new Pool({ connectionString: env.DB_SYNC_CONNECTION_STRING });

try {
logger.info('About to query db for distinct addresses');
const result: QueryResult<AddressesModel> = await db.query(findAddressesWithRewardsHistory, [vars.walletsCount]);
logger.info('Found addresses count', result.rowCount);

vars.addresses = result.rows.map(mapToGroupedAddress);
} catch (error) {
ee.emit('counter', 'findAddresses.error', 1);
logger.error('Error thrown while performing findAddresses db sync query', error);
}

if (vars.addresses.length < vars.walletsCount) {
logger.error(SHORTAGE_OF_WALLETS_FOUND_ERROR_MESSAGE);
throw new Error(SHORTAGE_OF_WALLETS_FOUND_ERROR_MESSAGE);
}
done();
};

export const walletRestoration: FunctionHook<WalletVars> = async ({ vars, _uid }, ee, done) => {
let index = 0;
return (async () => {
const currentAddress = vars.addresses[index];

try {
const keyAgent = new StubKeyAgent(currentAddress);

// Start to measure wallet restoration time
const startedAt = Date.now();
const { wallet } = await getWallet({
env,
idx: 0,
keyAgent,
logger,
name: `Test Wallet of VU with id: ${_uid}`,
polling: { interval: 50 }
});
await walletReady(wallet);
vars.currentWallet = wallet;

// Emit custom metrics
ee.emit('histogram', `${operationName}.time`, Date.now() - startedAt);
ee.emit('counter', operationName, 1);

logger.info(`Wallet with name ${wallet.name} is successfully restored`);
} catch (error) {
ee.emit('counter', `${operationName}.error`, 1);
logger.error(error);
}
++index;
done();
})();
};

export const shutdownWallet: FunctionHook<WalletVars> = async ({ vars }, _ee, done) => {
vars.currentWallet.shutdown();
done();
};
@@ -0,0 +1,22 @@
config:
# target is a required field for artillery, but it is actually
# ignored since we are not using artillery to perform the HTTP requests.
target: "http://localhost"
http:
timeout: 180
phases:
- name: 'Restore wallet'
# The duration of an arrival phase determines only how long virtual users will be generated for. It is not the same as the duration of a test run.
duration: "{{$processEnvironment.ARRIVAL_PHASE_DURATION_IN_SECS}}"
# Fixed number of arrivals (VUs) per second
arrivalCount: "{{$processEnvironment.WALLETS_COUNT}}"
processor: "./WalletRestoration.ts"
before:
flow:
- log: "Find distinct addresses from DB"
- function: "findAddresses"
scenarios:
- name: "Wallet restoration"
afterScenario: "shutdownWallet"
flow:
- function: "walletRestoration"
14 changes: 14 additions & 0 deletions packages/e2e/test/artillery/wallet-restoration/queries.ts
@@ -0,0 +1,14 @@
/**
* Query randomized distinct addresses from db associated with users who are staking
*/
export const findAddressesWithRewardsHistory = `
SELECT * FROM (
SELECT DISTINCT txOut.address as address, sa.view as stake_address
FROM public.delegation d
LEFT JOIN public.tx_out txOut on
d.addr_id = txOut.stake_address_id
LEFT JOIN public.stake_address sa on
txOut.stake_address_id = sa.id
) distinct_addresses
ORDER BY RANDOM() LIMIT $1
`;
16 changes: 16 additions & 0 deletions packages/e2e/test/artillery/wallet-restoration/types.ts
@@ -0,0 +1,16 @@
import { GroupedAddress } from '@cardano-sdk/key-management';
import { SingleAddressWallet } from '@cardano-sdk/wallet';

/**
* The context variables shared between all the hooks.
*/
export interface WalletVars {
walletsCount: number;
addresses: GroupedAddress[];
currentWallet: SingleAddressWallet;
}

export interface AddressesModel {
address: string;
stake_address: string;
}
4 changes: 3 additions & 1 deletion packages/e2e/test/environment.ts
Expand Up @@ -22,7 +22,9 @@ export const getEnv = (override: NodeJS.ProcessEnv = {}) =>
TX_SUBMIT_PROVIDER: envalid.str(),
TX_SUBMIT_PROVIDER_PARAMS: envalid.json({ default: {} }),
UTXO_PROVIDER: envalid.str(),
UTXO_PROVIDER_PARAMS: envalid.json({ default: {} })
UTXO_PROVIDER_PARAMS: envalid.json({ default: {} }),
// Should be moved once env types are refactored in ADP-2334
WALLETS_COUNT: envalid.num()
}
);

Expand Down

0 comments on commit 9be492d

Please sign in to comment.