Skip to content

Commit

Permalink
Merge pull request #528 from input-output-hk/feat/wallet-restoration-…
Browse files Browse the repository at this point in the history
…load-test

feat(e2e): wallet restoration load test
  • Loading branch information
rhyslbw committed Nov 29, 2022
2 parents 5bdbbc9 + 279a538 commit 77af399
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 8 deletions.
16 changes: 14 additions & 2 deletions packages/e2e/README.md
Expand Up @@ -34,7 +34,7 @@ $ yarn workspace @cardano-sdk/e2e generate-mnemonics
And you will get the set of mnemonics plus the first derivative address on the console:

```bash
$ ts-node ./src/util/mnemonic.ts
$ ts-node ./src/scripts/mnemonic.ts

Mnemonic: toward bridge spell endless tunnel there deputy market scheme ketchup heavy fall fault pudding split desert swear maximum orchard estate match good decorate tribe

Expand Down Expand Up @@ -273,8 +273,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} VIRTUAL_USERS_COUNT={wallets count to restore} yarn artillery:wallet-restoration
```
6 changes: 4 additions & 2 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:load-testing": "jest -c jest.config.js --forceExit --selectProjects load-testing --runInBand --verbose",
"test:long-running": "jest -c jest.config.js --forceExit --selectProjects long-running --runInBand --verbose",
Expand Down Expand Up @@ -47,8 +48,8 @@
"prepack": "yarn build",
"test:build:verify": "yarn test:web-extension:build",
"test:debug": "DEBUG=true yarn test",
"generate-mnemonics": "ts-node src/util/mnemonic.ts",
"wait-for-network": "ts-node src/util/is-local-network-ready.ts"
"generate-mnemonics": "ts-node src/scripts/mnemonic.ts",
"wait-for-network": "ts-node src/scripts/is-local-network-ready.ts"
},
"repository": "https://github.com/input-output-hk/cardano-js-sdk",
"contributors": [
Expand Down Expand Up @@ -89,6 +90,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';
File renamed without changes.
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');
}
}
1 change: 1 addition & 0 deletions packages/e2e/src/util/index.ts
@@ -0,0 +1 @@
export * from './StubKeyAgent';
@@ -0,0 +1,93 @@
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 { findAddressesWithRegisteredStakeKey } from './queries';
import { getEnv, walletVariables } from '../../environment';
import { logger } from '@cardano-sdk/util-dev';
import { walletReady } from '../../util';

const env = getEnv([
...walletVariables,
'DB_SYNC_CONNECTION_STRING',
'ARRIVAL_PHASE_DURATION_IN_SECS',
'VIRTUAL_USERS_COUNT'
]);

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.VIRTUAL_USERS_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(findAddressesWithRegisteredStakeKey, [
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 (virtual users)
arrivalCount: "{{$processEnvironment.VIRTUAL_USERS_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 findAddressesWithRegisteredStakeKey = `
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;
}
2 changes: 2 additions & 0 deletions packages/e2e/test/environment.ts
Expand Up @@ -56,6 +56,7 @@ const providerParams = makeValidator((value) => {
* Shared across all tests
*/
const validators = {
ARRIVAL_PHASE_DURATION_IN_SECS: num(),
ASSET_PROVIDER: str(),
ASSET_PROVIDER_PARAMS: providerParams(),
CHAIN_HISTORY_PROVIDER: str(),
Expand All @@ -79,6 +80,7 @@ const validators = {
TX_SUBMIT_PROVIDER_PARAMS: providerParams(),
UTXO_PROVIDER: str(),
UTXO_PROVIDER_PARAMS: providerParams(),
VIRTUAL_USERS_COUNT: num(),
WORKER_PARALLEL_TRANSACTION: num()
} as const;

Expand Down
1 change: 1 addition & 0 deletions yarn-project.nix
Expand Up @@ -1315,6 +1315,7 @@ cacheEntries = {
"fs-minipass@npm:2.1.0" = { filename = "fs-minipass-npm-2.1.0-501ef87306-1b8d128dae.zip"; sha512 = "1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1"; };
"fs.realpath@npm:1.0.0" = { filename = "fs.realpath-npm-1.0.0-c8f05d8126-99ddea01a7.zip"; sha512 = "99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0"; };
"fsevents@npm:2.3.2" = { filename = "fsevents-npm-2.3.2-a881d6ac9f-97ade64e75.zip"; sha512 = "97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f"; };
"fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7" = { filename = "fsevents-patch-3340e2eb10-8.zip"; sha512 = "edbd0fd80be379c14409605f77e52fdc78a119e17f875e8b90a220c3e5b29e54a1477c21d91fd30b957ea4866406dc3ff87b61432d2840ff8866b309e5866140"; };
"ftp@npm:0.3.10" = { filename = "ftp-npm-0.3.10-348fb9ac23-ddd313c1d4.zip"; sha512 = "ddd313c1d44eb7429f3a7d77a0155dc8fe86a4c64dca58f395632333ce4b4e74c61413c6e0ef66ea3f3d32d905952fbb6d028c7117d522f793eb1fa282e17357"; };
"function-bind@npm:1.1.1" = { filename = "function-bind-npm-1.1.1-b56b322ae9-b32fbaebb3.zip"; sha512 = "b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a"; };
"function.prototype.name@npm:1.1.5" = { filename = "function.prototype.name-npm-1.1.5-e776a642bb-acd21d733a.zip"; sha512 = "acd21d733a9b649c2c442f067567743214af5fa248dbeee69d8278ce7df3329ea5abac572be9f7470b4ec1cd4d8f1040e3c5caccf98ebf2bf861a0deab735c27"; };
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Expand Up @@ -2530,6 +2530,7 @@ __metadata:
mock-browser: ^0.92.14
npm-run-all: ^4.1.5
optionator: ^0.9.1
pg: ^8.7.3
readable-stream: ^3.6.0
rxjs: ^7.4.0
shx: ^0.3.3
Expand Down

0 comments on commit 77af399

Please sign in to comment.