Integration tests: SSL service, RocksDB in the CI matrix

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-23 16:28:37 +02:00
1 parent cba58d7e25
commit e30364c21b
4 files changed
+47 -11

No files matched your search

+9 -4
View File
@@ -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
+3 -3
View File
@@ -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 `<coin>_<net>_<id>.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 <hash> 0|1`, `getrawtransaction <txid> true <blockhash>`, 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 `<coin>_<net>_<id>.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 <hash> 0|1`, `getrawtransaction <txid> true <blockhash>`, 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
+17 -2
View File
@@ -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,
+18 -2
View File
@@ -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())