Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kevin/load test erc20 token #1577

Merged
merged 8 commits into from
Jul 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 83 additions & 8 deletions priv/perf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,89 @@

Umbrella app for performance/load/stress tests

## How to run the test
1. Change the urls inside the [config files](config/) for the environment you are targeting.
1. To generate the open-api client (only need once): `make init`
1. To run the test: `MIX_ENV=<your_env> mix test`. Or `mix test` if you want to run against local services.
## How to run the tests

### Spin up local services for development
1. To test with local services (by docker-compose): `make start-services`
1. To turn the local services down: `make stop-services`
### 1. Set up the environment vars

```
export CHILD_CHAIN_URL=<childchain api url>
export WATCHER_INFO_URL=<watcher-info api url>
export ETHEREUM_RPC_URL=<ethereum node url>
export CONTRACT_ADDRESS_PLASMA_FRAMEWORK=<address of the plasma framework contract>
export CONTRACT_ADDRESS_ETH_VAULT=<address of the eth vault contract>
export CONTRACT_ADDRESS_ERC20_VAULT=<address of the erc20 vault contract>
export LOAD_TEST_FAUCET_PRIVATE_KEY=<faucet private key>
```


### 2. Generate the open-api client
```
make init
```

### 3. Configure the tests
Edit the config file (e.g. `config/dev.exs`) set the test parameters e.g.
```
childchain_transactions_test_config: %{
concurrent_sessions: 100,
transactions_per_session: 600,
transaction_delay: 1000
}
```

Note that by default the tests use ETH both as the currency spent and as the fee.
This makes the code simpler as it doesn't have to manage separate fee utxos.
However, if necessary you can configure the tests to use a different currency. e.g.
```
config :load_test,
test_currency: "0x942f123b3587EDe66193aa52CF2bF9264C564F87",
fee_amount: 6_000_000_000_000_000,
```

### 4. Run the tests
```
MIX_ENV=<your_env> mix test
```

Or just `mix test` if you want to run against local services.

You can specify a particular test on the command line e.g.

```
MIX_ENV=dev mix test apps/load_test/test/load_tests/runner/childchain_test.exs
```

**Important** After each test run, you need to wait ~15 seconds before running it again.
This is necessary to wait for the faucet account's utxos to be spendable.
Depending on the watcher-info load, it can take longer than this.

If you get an error like this
```
module=LoadTest.Service.Faucet Funding user 0x76f0a3aade31c19d306bc91b46817b95072a8cbd with 2 from utxo: 10800070000⋅
module=LoadTest.ChildChain.Transaction Transaction submission has failed, reason: "submit:utxo_not_found"⋅
```

then you haven't waited long enough.
Kill it, wait some more, try again.

### Increase connection pool size and connection
One can override the setup in config to increase the `pool_size` and `max_connection`. If you found the latency on the api calls are high but the data dog latency shows way smaller, it might be latency from setting up the connection instead of real api latency.
One can override the setup in config to increase the `pool_size` and `max_connection`.
If you found the latency on the api calls are high but the data dog latency shows way smaller,
it might be latency from setting up the connection instead of real api latency.

### Retrying on errors
The Tesla HTTP middleware can be configured to retry on error.
By default this is disabled, but it can be enabled by modifying the `retry?` function in `connection_defaults.ex`.

For example, to retry any 500 response:
```
defp retry?() do
fn
{:ok, %{status: status}} when status in 500..599 -> true
{:ok, _} -> false
{:error, _} -> false
end
end
```

See [Tesla.Middleware.Retry](https://hexdocs.pm/tesla/Tesla.Middleware.Retry.html) for more details.
5 changes: 3 additions & 2 deletions priv/perf/apps/load_test/lib/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule LoadTest.Application do
:hackney_pool.start_pool(
LoadTest.Connection.ConnectionDefaults.pool_name(),
timeout: 180_000,
connect_timeout: 30_000,
pool_size: pool_size,
max_connections: max_connections
)
Expand All @@ -30,8 +31,8 @@ defmodule LoadTest.Application do
defp fetch_faucet_config() do
faucet_config_keys = [
:faucet_private_key,
:fee_wei,
:faucet_deposit_wei,
:fee_amount,
:faucet_deposit_amount,
:deposit_finality_margin,
:gas_price
]
Expand Down
57 changes: 47 additions & 10 deletions priv/perf/apps/load_test/lib/child_chain/deposit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,21 @@ defmodule LoadTest.ChildChain.Deposit do

@eth <<0::160>>
@poll_interval 5_000
@doc """
Deposits funds into the childchain.

If currency is ETH, funds will be deposited into the EthVault.
If currency is ERC20, 'approve()' will be called before depositing funds into the Erc20Vault.

Returns the utxo created by the deposit.
"""
@spec deposit_from(
LoadTest.Ethereum.Account.t(),
pos_integer(),
LoadTest.Ethereum.Account.t(),
pos_integer(),
pos_integer()
) :: Utxo.t()
def deposit_from(%Account{} = depositor, amount, currency, deposit_finality_margin, gas_price) do
deposit_utxo = %Utxo{amount: amount, owner: depositor.addr, currency: currency}
{:ok, deposit} = Deposit.new(deposit_utxo)
Expand All @@ -36,15 +50,29 @@ defmodule LoadTest.ChildChain.Deposit do
end

defp send_deposit(deposit, account, value, @eth, gas_price) do
eth_vault_address = Application.fetch_env!(:load_test, :eth_vault_address)
vault_address = Application.fetch_env!(:load_test, :eth_vault_address)
do_deposit(vault_address, deposit, account, value, gas_price)
end

defp send_deposit(deposit, account, value, erc20_contract, gas_price) do
vault_address = Application.fetch_env!(:load_test, :erc20_vault_address)

# First have to approve the token
{:ok, tx_hash} = approve(erc20_contract, vault_address, account, value, gas_price)
{:ok, _} = Ethereum.transact_sync(tx_hash)

# Note that when depositing erc20 tokens, then tx value must be 0
do_deposit(vault_address, deposit, account, 0, gas_price)
end

defp do_deposit(vault_address, deposit, account, value, gas_price) do
%{data: deposit_data} = LoadTest.Utils.Encoding.encode_deposit(deposit)

tx = %LoadTest.Ethereum.Transaction{
to: Encoding.to_binary(eth_vault_address),
to: Encoding.to_binary(vault_address),
value: value,
gas_price: gas_price,
gas_limit: 200_000,
init: <<>>,
data: Encoding.to_binary(deposit_data)
}

Expand All @@ -53,14 +81,10 @@ defmodule LoadTest.ChildChain.Deposit do

{:ok, %{"logs" => logs}} = Ethereumex.HttpClient.eth_get_transaction_receipt(tx_hash)

deposit_blknum =
logs
|> Enum.map(fn %{"topics" => topics} -> topics end)
|> Enum.map(fn [_topic, _addr, blknum | _] -> blknum end)
|> hd()
|> Encoding.to_int()
%{"topics" => [_topic, _addr, blknum | _]} =
Enum.find(logs, fn %{"address" => address} -> address == vault_address end)

{:ok, {deposit_blknum, eth_blknum}}
{:ok, {Encoding.to_int(blknum), eth_blknum}}
end

defp wait_deposit_finality(deposit_eth_blknum, finality_margin) do
Expand All @@ -75,4 +99,17 @@ defmodule LoadTest.ChildChain.Deposit do
wait_deposit_finality(deposit_eth_blknum, finality_margin)
end
end

defp approve(contract, vault_address, account, value, gas_price) do
data = ABI.encode("approve(address,uint256)", [Encoding.to_binary(vault_address), value])

tx = %LoadTest.Ethereum.Transaction{
to: contract,
gas_price: gas_price,
gas_limit: 200_000,
data: data
}

Ethereum.send_raw_transaction(tx, account)
end
end
25 changes: 17 additions & 8 deletions priv/perf/apps/load_test/lib/child_chain/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ defmodule LoadTest.ChildChain.Transaction do
alias LoadTest.Connection.ChildChain, as: Connection

@retry_interval 1_000
@eth <<0::160>>

@doc """
Spends a utxo.
Expand All @@ -46,20 +45,28 @@ defmodule LoadTest.ChildChain.Transaction do
Utxo.address_binary(),
pos_integer()
) :: list(Utxo.t())
def spend_utxo(utxo, amount, fee, signer, receiver, currency \\ @eth, retries \\ 0) do
def spend_utxo(utxo, amount, fee, signer, receiver, currency, retries \\ 0)

def spend_utxo(utxo, amount, fee, signer, receiver, currency, retries) when byte_size(currency) == 20 do
change_amount = utxo.amount - amount - fee
receiver_output = %Utxo{owner: receiver.addr, currency: currency, amount: amount}
do_spend(utxo, receiver_output, change_amount, signer, retries)
do_spend(utxo, receiver_output, change_amount, currency, signer, retries)
end

defp do_spend(_input, _output, change_amount, _signer, _retries) when change_amount < 0, do: :error_insufficient_funds
def spend_utxo(utxo, amount, fee, signer, receiver, currency, retries) do
spend_utxo(utxo, amount, fee, signer, receiver, Encoding.to_binary(currency), retries)
end

defp do_spend(input, output, 0, signer, retries) do
defp do_spend(_input, _output, change_amount, _currency, _signer, _retries) when change_amount < 0 do
:error_insufficient_funds
end

defp do_spend(input, output, 0, _currency, signer, retries) do
submit_tx([input], [output], [signer], retries)
end

defp do_spend(input, output, change_amount, signer, retries) do
change_output = %Utxo{owner: signer.addr, currency: @eth, amount: change_amount}
defp do_spend(input, output, change_amount, currency, signer, retries) do
change_output = %Utxo{owner: signer.addr, currency: currency, amount: change_amount}
submit_tx([input], [change_output, output], [signer], retries)
end

Expand Down Expand Up @@ -125,7 +132,9 @@ defmodule LoadTest.ChildChain.Transaction do
{:ok, blknum, txindex}

%{"code" => reason} ->
_ = Logger.warn("Transaction submission has failed, reason: #{inspect(reason)}")
_ =
Logger.warn("Transaction submission has failed, reason: #{inspect(reason)}, tx inputs: #{inspect(tx.inputs)}")

{:error, reason}
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ defmodule LoadTest.Connection.ConnectionDefaults do
"""
def pool_name(), do: :perf_pool

# Don't automatically retry on error
# It _can_ sometimes be useful to retry though, so if you need it return true here
# See README.md for more info
defp retry?() do
fn
{:ok, %{status: status}} when status in 500..599 -> true
{:ok, _} -> false
{:error, _} -> true
_ -> false
end
end
end
8 changes: 5 additions & 3 deletions priv/perf/apps/load_test/lib/runner/childchain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ defmodule LoadTest.Runner.ChildChainTransactions do
end

def scenarios() do
fee_wei = Application.fetch_env!(:load_test, :fee_wei)
test_currency = Application.fetch_env!(:load_test, :test_currency)
fee_amount = Application.fetch_env!(:load_test, :fee_amount)
config = default_config()

{:ok, sender} = Account.new()
Expand All @@ -52,7 +53,7 @@ defmodule LoadTest.Runner.ChildChainTransactions do
amount = 1

ntx_to_send = config.transactions_per_session
initial_funds = (amount + fee_wei) * ntx_to_send
initial_funds = (amount + fee_amount) * ntx_to_send

[
{{config.concurrent_sessions, [LoadTest.Scenario.FundAccount, LoadTest.Scenario.SpendEthUtxo]},
Expand All @@ -61,7 +62,8 @@ defmodule LoadTest.Runner.ChildChainTransactions do
initial_funds: initial_funds,
sender: sender,
receiver: receiver,
amount: amount
amount: amount,
test_currency: test_currency
}}
]
end
Expand Down
4 changes: 2 additions & 2 deletions priv/perf/apps/load_test/lib/scenario/account_transactions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ defmodule LoadTest.Scenario.AccountTransactions do

def run(session) do
iterations = config(session, [:iterations])
fee_wei = Application.fetch_env!(:load_test, :fee_wei)
fee_amount = Application.fetch_env!(:load_test, :fee_amount)

amount = iterations * (@test_output_amount + fee_wei)
amount = iterations * (@test_output_amount + fee_amount)
{:ok, sender} = Account.new()
{:ok, _} = Faucet.fund_child_chain_account(sender, amount, @eth)

Expand Down
38 changes: 24 additions & 14 deletions priv/perf/apps/load_test/lib/scenario/create_utxos.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,49 +26,59 @@ defmodule LoadTest.Scenario.CreateUtxos do
alias Chaperon.Session
alias ExPlasma.Utxo

@eth <<0::160>>
@spawned_outputs_per_transaction 3

@spec run(Session.t()) :: Session.t()
def run(session) do
fee_wei = Application.fetch_env!(:load_test, :fee_wei)
session = Session.assign(session, fee_wei: fee_wei)
fee_amount = Application.fetch_env!(:load_test, :fee_amount)
session = Session.assign(session, fee_amount: fee_amount)
test_currency = Application.fetch_env!(:load_test, :test_currency)
session = Session.assign(session, test_currency: test_currency)

sender = config(session, [:sender])
utxos_to_create_per_session = config(session, [:utxos_to_create_per_session])
number_of_transactions = div(utxos_to_create_per_session, 3)

transactions_per_session = config(session, [:transactions_per_session])
min_final_change = transactions_per_session * fee_wei + 1
min_final_change = transactions_per_session * fee_amount + 1

amount_per_utxo = get_amount_per_created_utxo(fee_wei)
initial_funds = number_of_transactions * fee_wei + utxos_to_create_per_session * amount_per_utxo + min_final_change
amount_per_utxo = get_amount_per_created_utxo(fee_amount)

initial_funds =
number_of_transactions * fee_amount + utxos_to_create_per_session * amount_per_utxo + min_final_change

session
|> run_scenario(LoadTest.Scenario.FundAccount, %{
account: sender,
initial_funds: initial_funds
initial_funds: initial_funds,
test_currency: test_currency
})
|> repeat(:submit_transaction, [sender], number_of_transactions)
end

def submit_transaction(session, sender) do
{inputs, outputs} = create_transaction(sender, session.assigned.utxo, session.assigned.fee_wei)
{inputs, outputs} =
create_transaction(
sender,
session.assigned.utxo,
session.assigned.test_currency,
session.assigned.fee_amount
)

new_outputs = LoadTest.ChildChain.Transaction.submit_tx(inputs, outputs, [sender])

Session.assign(session, utxo: List.last(new_outputs))
end

defp create_transaction(sender, input, fee_wei) do
amount_per_utxo = get_amount_per_created_utxo(fee_wei)
change = input.amount - @spawned_outputs_per_transaction * amount_per_utxo - fee_wei
defp create_transaction(sender, input, currency, fee_amount) do
amount_per_utxo = get_amount_per_created_utxo(fee_amount)
change = input.amount - @spawned_outputs_per_transaction * amount_per_utxo - fee_amount

created_output = %Utxo{owner: sender.addr, currency: @eth, amount: amount_per_utxo}
change_output = %Utxo{owner: sender.addr, currency: @eth, amount: change}
created_output = %Utxo{owner: sender.addr, currency: currency, amount: amount_per_utxo}
change_output = %Utxo{owner: sender.addr, currency: currency, amount: change}

{[input], List.duplicate(created_output, @spawned_outputs_per_transaction) ++ [change_output]}
end

defp get_amount_per_created_utxo(fee_wei), do: fee_wei + 2
defp get_amount_per_created_utxo(fee_amount), do: fee_amount + 2
end
Loading