From e30364c21b905e09253e7acd9b5170d3f0841820 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 23 Oct 2026 16:28:37 +0200 Subject: [PATCH] Integration tests: SSL service, RocksDB in the CI matrix Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 13 +++++++++---- CLAUDE.md | 6 +++--- tests/integration/conftest.py | 19 +++++++++++++++++-- tests/integration/test_protocol.py | 20 ++++++++++++++++++-- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6204f3e..8350f16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,9 +106,13 @@ jobs: run: contrib/docker-smoke-test.sh plm-electrumx integration: - # ElectrumX against a real Palladium node in regtest mode (tests/integration) - name: integration + # ElectrumX against a real Palladium node in regtest mode (tests/integration), with each DB engine + name: integration (${{ matrix.db_engine }}) runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + db_engine: [leveldb, rocksdb] steps: - uses: actions/checkout@v6 @@ -121,12 +125,12 @@ jobs: - name: Install system packages run: | sudo apt-get -qq update - sudo apt-get install -yq libleveldb-dev + sudo apt-get install -yq libsnappy-dev zlib1g-dev libbz2-dev libgflags-dev liblz4-dev librocksdb-dev libleveldb-dev - name: Install Python deps run: | pip install pytest - pip install ".[leveldb]" + pip install ".[${{ matrix.db_engine }}]" - name: Start the regtest node run: docker compose -f tests/integration/docker-compose.yml up -d --build --wait @@ -134,6 +138,7 @@ jobs: - name: Run integration tests env: PLM_TEST_DAEMON_URL: http://plmtest:plmtest@127.0.0.1:28443/ + PLM_TEST_DB_ENGINE: ${{ matrix.db_engine }} run: pytest tests/integration - name: Node log diff --git a/CLAUDE.md b/CLAUDE.md index 162b2f3..7730911 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,15 +41,15 @@ Pipeline (see `docs/architecture.rst`): `Env` → `Controller` (`server/controll Coin abstraction: `lib/coins.py` defines `Coin` and the `Palladium`, `PalladiumTestnet`, `PalladiumRegtest` subclasses. A coin class selects behavior through class attributes: `DESERIALIZER` (block/tx format, in `lib/tx.py`, only the segwit one remains), `DAEMON`, `BLOCK_PROCESSOR`, `SESSIONCLS`, address verbytes, `GENESIS_HASH`, `REORG_LIMIT`, `RPC_PORT`, `PEERS`, header handling, etc. Coins are discovered by walking `Coin` subclasses (`lookup_coin_class` matches `NAME` + `NET`; `COIN` defaults to `Palladium`). Concrete classes must define `TX_COUNT`, `TX_COUNT_HEIGHT`, `TX_PER_BLOCK` (used only for sync ETAs) or lookup raises. -Daemon: Palladium Core has segwit and taproot (BIP341/342, mainnet height 405000; the server is script-type agnostic, indexing by script hash) and bech32/bech32m addresses, but no `getindexinfo`, `txospenderindex` or `submitpackage` RPCs. So `MIN_REQUIRED_DAEMON_VERSION`/`REQUIRED_DAEMON_INDEXES` (see `Coin`) are unset, and `blockchain.outpoint.subscribe` is refused with an explicit error (`Coin.DAEMON_HAS_TXO_SPENDER_INDEX = False`: otherwise the subscription is accepted and the daemon error at the next block closes the session) while `blockchain.transaction.broadcast_package` returns the daemon error to clients. The daemon needs `txindex=1`. +Daemon: Palladium Core has segwit and taproot (BIP341/342, mainnet height 405000; the server is script-type agnostic, indexing by script hash) and bech32/bech32m addresses, but no `getindexinfo`, `txospenderindex` or `submitpackage` RPCs. So `MIN_REQUIRED_DAEMON_VERSION`/`REQUIRED_DAEMON_INDEXES` (see `Coin`) are unset, and `blockchain.outpoint.subscribe` is refused with an explicit error (`Coin.DAEMON_HAS_TXO_SPENDER_INDEX = False`: otherwise the subscription is accepted and the daemon error at the next block closes the session) while `blockchain.transaction.broadcast_package` returns an explicit "not supported" error (the daemon answers method not found). The daemon needs `txindex=1`. DB schema changed in 2.0 with no migration; changing on-disk format requires a full resync. ## Tests -Integration tests (`tests/integration`): `node/Dockerfile` + `node/palladium.conf` run the Palladium node (official release binaries, signature checked) in regtest, `conftest.py` starts `electrumx_server` on an empty DB against it and the tests mine blocks and check headers, balances, mempool, history, merkle proofs and broadcast through the Electrum protocol. Regtest coinbase maturity is 120 blocks, bech32 hrp is `rplm` (`plm` on mainnet, `tplm` on testnet: `Coin.BECH32_HRP`). The CI job is `integration`. Bump `VERSION` in the Dockerfile to test against a newer node. +Integration tests (`tests/integration`): `node/Dockerfile` + `node/palladium.conf` run the Palladium node (official release binaries, signature checked) in regtest, `conftest.py` starts `electrumx_server` on an empty DB against it and the tests mine blocks and check headers, balances, mempool, history, merkle proofs and broadcast through the Electrum protocol. Regtest coinbase maturity is 120 blocks, bech32 hrp is `rplm` (`plm` on mainnet, `tplm` on testnet: `Coin.BECH32_HRP`). The CI job is `integration` (LevelDB and RocksDB; `PLM_TEST_DB_ENGINE` selects the engine locally). Bump `VERSION` in the Dockerfile to test against a newer node. -`tests/lib` (Palladium params, addresses, hashes, scripts), `tests/server` (env, daemon, mempool, storage, API), plus fixture-driven `tests/test_blocks.py` / `test_transactions.py` using JSON files named `__.json` in `tests/blocks` and `tests/transactions` (real mainnet data from a synced node; tx vout values in satoshis). `test_storage.py` needs both DB engines installed. Fixtures were taken from a synced node (`getblock 0|1`, `getrawtransaction true `, run via `docker compose exec palladiumd palladium-cli ...` in the node's project); vout values converted to satoshis. Ports, SSL, RPC credentials and donation address are documented in `DEPLOY.md`. +`tests/lib` (Palladium params, addresses, hashes, scripts), `tests/server` (env, daemon, mempool, storage, API), plus fixture-driven `tests/test_blocks.py` / `test_transactions.py` using JSON files named `__.json` in `tests/blocks` and `tests/transactions` (real mainnet data from a synced node; tx vout values in satoshis). `test_storage.py` needs both DB engines installed. Fixtures (including a taproot spend, `palladium_mainnet_2a74f4`) were taken from a synced node (`getblock 0|1`, `getrawtransaction true `, run via `docker compose exec palladiumd palladium-cli ...` in the node's project); vout values converted to satoshis. Ports, SSL, RPC credentials and donation address are documented in `DEPLOY.md`. ## Repository and workflow diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 1f8cba5..1883dd9 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -11,6 +11,7 @@ import base64 import json import os import socket +import ssl import subprocess import sys import tempfile @@ -100,8 +101,10 @@ def scripthash(node, address): class ElectrumClient: '''Newline-delimited JSON-RPC over TCP, one request at a time (notifications are skipped).''' - def __init__(self, port): + def __init__(self, port, ssl_context=None): self.sock = socket.create_connection(('127.0.0.1', port), timeout=30) + if ssl_context: + self.sock = ssl_context.wrap_socket(self.sock, server_hostname='localhost') self.file = self.sock.makefile('rwb') self.next_id = 0 @@ -122,6 +125,10 @@ class ElectrumClient: self.sock.close() +# set by the electrumx fixture: the SSL port of the server under test and the certificate it presents +SSL_SERVICE = {} + + def free_port(): with socket.socket() as s: s.bind(('127.0.0.1', 0)) @@ -136,11 +143,19 @@ def electrumx(node, wallet): if node.call('getblockcount') < MIN_HEIGHT: node.mine(MIN_HEIGHT - node.call('getblockcount'), miner) port = free_port() + ssl_port = free_port() with tempfile.TemporaryDirectory() as db_dir: + certfile = os.path.join(db_dir, 'server.crt') + keyfile = os.path.join(db_dir, 'server.key') + subprocess.run(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-days', '1', '-subj', '/CN=localhost', + '-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1', '-keyout', keyfile, '-out', certfile], + check=True, capture_output=True) + SSL_SERVICE.update(port=ssl_port, certfile=certfile) env = dict(os.environ, COIN='Palladium', NET='regtest', DB_ENGINE=os.environ.get('PLM_TEST_DB_ENGINE', 'leveldb'), DB_DIRECTORY=db_dir, - DAEMON_URL=DAEMON_URL, SERVICES=f'tcp://127.0.0.1:{port}', + DAEMON_URL=DAEMON_URL, SERVICES=f'tcp://127.0.0.1:{port},ssl://127.0.0.1:{ssl_port}', + SSL_CERTFILE=certfile, SSL_KEYFILE=keyfile, PEER_DISCOVERY='off', PEER_ANNOUNCE='', LOG_LEVEL='info') log = open(os.path.join(db_dir, 'electrumx.log'), 'w+') proc = subprocess.Popen([sys.executable, str(REPO_ROOT / 'electrumx_server')], cwd=REPO_ROOT, env=env, diff --git a/tests/integration/test_protocol.py b/tests/integration/test_protocol.py index 2dd7270..8c6740b 100644 --- a/tests/integration/test_protocol.py +++ b/tests/integration/test_protocol.py @@ -1,7 +1,9 @@ '''Electrum protocol methods per protocol version, daemon limits and reorgs, against the regtest node (see conftest.py).''' import pytest -from tests.integration.conftest import ElectrumClient, new_address, scripthash, wait_for +import ssl + +from tests.integration.conftest import SSL_SERVICE, ElectrumClient, new_address, scripthash, wait_for @pytest.fixture @@ -60,7 +62,7 @@ def test_daemon_limits_are_reported_as_errors(client): '''The Palladium daemon has no submitpackage and no gettxspendingprevout: clients get an error. If a newer daemon gains them, update CLAUDE.md, README.md and DEPLOY.md and this test.''' negotiate(client, '1.7') - with pytest.raises(RuntimeError, match='rejected by network rules'): + with pytest.raises(RuntimeError, match='does not support package submission'): client.call('blockchain.transaction.broadcast_package', ['00']) # a subscription would be accepted for an outpoint in the mempool and then close the session at the next block with pytest.raises(RuntimeError, match='outpoint subscriptions are not supported'): @@ -116,3 +118,17 @@ def test_reorg_replaces_blocks(node, wallet, electrumx, client): 'the balance after the reorg') history = client.call('blockchain.scripthash.get_history', sh) assert [(h['tx_hash'], h['height']) for h in history] == [(txid, height)] + + +def test_ssl_service(node, electrumx): + '''The SSL port serves the same protocol, with the certificate given by SSL_CERTFILE/SSL_KEYFILE''' + context = ssl.create_default_context(cafile=SSL_SERVICE['certfile']) + client = ElectrumClient(SSL_SERVICE['port'], context) + try: + assert negotiate(client, '1.4')[1] == '1.4' + assert client.call('blockchain.block.header', 0) + finally: + client.close() + # a client that does not trust the (self-signed) certificate refuses it + with pytest.raises(ssl.SSLCertVerificationError): + ElectrumClient(SSL_SERVICE['port'], ssl.create_default_context())