Integration tests: SSL service, RocksDB in the CI matrix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cba58d7e25
commit
e30364c21b
4 files changed
+47
-11
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user