From 75fa3e8ff4c3f046b4e5a3d42491c8838e0bb79c Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:56:26 +0200 Subject: [PATCH] Add initial set of performance tests --- tests_perf/README.md | 41 ++++++++++++++++ tests_perf/__init__.py | 0 tests_perf/conftest.py | 18 ++++++++ tests_perf/requirements.txt | 6 +++ tests_perf/setup.cfg | 20 ++++++++ tests_perf/test_perf_sign_psbt.py | 77 +++++++++++++++++++++++++++++++ 6 files changed, 162 insertions(+) create mode 100644 tests_perf/README.md create mode 100644 tests_perf/__init__.py create mode 100644 tests_perf/conftest.py create mode 100644 tests_perf/requirements.txt create mode 100644 tests_perf/setup.cfg create mode 100644 tests_perf/test_perf_sign_psbt.py diff --git a/tests_perf/README.md b/tests_perf/README.md new file mode 100644 index 000000000..bb75f6f93 --- /dev/null +++ b/tests_perf/README.md @@ -0,0 +1,41 @@ +# Benchmarks + +The tests in this folder are meant to measure the performance of various app operations. + +These tests are implemented in Python and can be executed either using the [Speculos](https://github.com/LedgerHQ/speculos) emulator or a Ledger Nano S+, Nano X, or Stax. + +Python dependencies are listed in [requirements.txt](requirements.txt), install them using [pip](https://pypi.org/project/pip/) + +``` +pip install -r requirements.txt +``` + +## Build + +The app must be built with the `AUTOAPPROVE_FOR_PERF_TESTS=1` parameter when calling `make`. This flag compiles the testnet app in a mode that requires no user interaction at all. + +## Launch with Speculos + +Performance measured in speculos is not a good proxy of the performance on a real device. + +Simply run: + +``` +pytest +``` + +## Launch with your device + +Compile and install the app on your device as normal. + +To run the tests on your Ledger device, you also need to install an optional dependency + +``` +pip install ledgercomm[hid] +``` + +Be sure to have you device connected through USB and open on the bitcoin testnet app, sideloaded from the build above. + +``` +pytest --hid +``` diff --git a/tests_perf/__init__.py b/tests_perf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_perf/conftest.py b/tests_perf/conftest.py new file mode 100644 index 000000000..577e6475c --- /dev/null +++ b/tests_perf/conftest.py @@ -0,0 +1,18 @@ + +from pathlib import Path +from test_utils.fixtures import * +import random +import sys +import os + +absolute_path = os.path.dirname(os.path.abspath(__file__)) +relative_bitcoin_path = ('../bitcoin_client') +absolute_bitcoin_client_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), '../') +sys.path.append(os.path.join(absolute_path, relative_bitcoin_path)) + +from ledger_bitcoin import Chain # noqa: E402 + +TESTS_ROOT_DIR = Path(__file__).parent + +random.seed(0) # make sure tests are repeatable diff --git a/tests_perf/requirements.txt b/tests_perf/requirements.txt new file mode 100644 index 000000000..b4588b87c --- /dev/null +++ b/tests_perf/requirements.txt @@ -0,0 +1,6 @@ +bip32>=3.4,<4.0 +embit>=0.8.0,<0.9.0 +ledgercomm>=1.2.1,<2.0.0 +pytest>=8.2.2,<9.0.0 +pytest-benchmark>=4.0.0,<5.0.0 +typing-extensions>=3.7,<4.0 diff --git a/tests_perf/setup.cfg b/tests_perf/setup.cfg new file mode 100644 index 000000000..2d726e2b4 --- /dev/null +++ b/tests_perf/setup.cfg @@ -0,0 +1,20 @@ +[tool:pytest] +addopts = --strict-markers + +[pylint] +disable = C0114, # missing-module-docstring + C0115, # missing-class-docstring + C0116, # missing-function-docstring + C0103, # invalid-name + R0801, # duplicate-code + R0913 # too-many-arguments +extension-pkg-whitelist=hid + +[pycodestyle] +max-line-length = 120 + +[mypy-hid.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True diff --git a/tests_perf/test_perf_sign_psbt.py b/tests_perf/test_perf_sign_psbt.py new file mode 100644 index 000000000..8cbb82121 --- /dev/null +++ b/tests_perf/test_perf_sign_psbt.py @@ -0,0 +1,77 @@ + +from pathlib import Path + +import pytest + +from ledger_bitcoin import WalletPolicy, Client +from ledger_bitcoin.psbt import PSBT + +from test_utils import txmaker + +tests_root: Path = Path(__file__).parent + + +def make_psbt(wallet_policy: WalletPolicy, n_inputs: int, n_outputs: int) -> PSBT: + in_amounts = [10000 + 10000 * i for i in range(n_inputs)] + total_in = sum(in_amounts) + out_amounts = [total_in // n_outputs - i for i in range(n_outputs)] + + change_index = 1 + + psbt = txmaker.createPsbt( + wallet_policy, + in_amounts, + out_amounts, + [i == change_index for i in range(n_outputs)] + ) + + sum_in = sum(in_amounts) + sum_out = sum(out_amounts) + + assert sum_out < sum_in + + return psbt + + +@pytest.mark.parametrize("n_inputs", [1, 3, 10]) +def test_perf_sign_psbt_singlesig_pkh(client: Client, n_inputs: int, benchmark): + # PSBT for a legacy 2-output spend (1 change address) + + wallet = WalletPolicy( + "", + "pkh(@0/**)", + [ + "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" + ], + ) + + psbt = make_psbt(wallet, n_inputs, 2) + + def sign_tx(): + result = client.sign_psbt(psbt, wallet, None) + + assert len(result) == n_inputs + + benchmark.pedantic(sign_tx, rounds=1) + + +@pytest.mark.parametrize("n_inputs", [1, 3, 10]) +def test_perf_sign_psbt_singlesig_wpkh(client: Client, n_inputs: int, benchmark): + # PSBT for a segwit 2-output spend (1 change address) + + wallet = WalletPolicy( + "", + "wpkh(@0/**)", + [ + "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P" + ], + ) + + psbt = make_psbt(wallet, n_inputs, 2) + + def sign_tx(): + result = client.sign_psbt(psbt, wallet, None) + + assert len(result) == n_inputs + + benchmark.pedantic(sign_tx, rounds=1)