Compare commits

...
12 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5.5 37d67385f4 Dockerfile: install rocksdb-ng in its own layer, so code changes do not recompile it
CI / unittests (ubuntu-24.04-arm) (push) Canceled after 0s
CI / unittests (ubuntu-24.04) (push) Canceled after 0s
CI / pycodestyle (push) Canceled after 0s
CI / build_docs (push) Canceled after 0s
CI / docker (ubuntu-24.04-arm) (push) Canceled after 0s
CI / docker (ubuntu-24.04) (push) Canceled after 0s
CI / integration (leveldb, ubuntu-24.04-arm) (push) Canceled after 0s
CI / integration (leveldb, ubuntu-24.04) (push) Canceled after 0s
CI / integration (rocksdb, ubuntu-24.04-arm) (push) Canceled after 0s
CI / integration (rocksdb, ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:47:40 +01:00
davideandClaude Sonnet 5.5 be689666cd README: architectures section rewritten without test measurements
CI / unittests (ubuntu-24.04) (push) Canceled after 0s
CI / unittests (ubuntu-24.04-arm) (push) Canceled after 0s
CI / pycodestyle (push) Canceled after 0s
CI / build_docs (push) Canceled after 0s
CI / docker (ubuntu-24.04) (push) Canceled after 0s
CI / docker (ubuntu-24.04-arm) (push) Canceled after 0s
CI / integration (leveldb, ubuntu-24.04) (push) Canceled after 0s
CI / integration (leveldb, ubuntu-24.04-arm) (push) Canceled after 0s
CI / integration (rocksdb, ubuntu-24.04) (push) Canceled after 0s
CI / integration (rocksdb, ubuntu-24.04-arm) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:32:04 +01:00
davideandClaude Sonnet 5.5 b53f9fdf5b README: full sync on amd64 against a mainnet node
CI / unittests (ubuntu-24.04-arm) (push) Canceled after 0s
CI / unittests (ubuntu-24.04) (push) Canceled after 0s
CI / pycodestyle (push) Canceled after 0s
CI / build_docs (push) Canceled after 0s
CI / docker (ubuntu-24.04-arm) (push) Canceled after 0s
CI / docker (ubuntu-24.04) (push) Canceled after 0s
CI / integration (leveldb, ubuntu-24.04-arm) (push) Canceled after 0s
CI / integration (leveldb, ubuntu-24.04) (push) Canceled after 0s
CI / integration (rocksdb, ubuntu-24.04-arm) (push) Canceled after 0s
CI / integration (rocksdb, ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 10:47:50 +01:00
davideandClaude Sonnet 5.5 82b864982d README: amd64 verified, amd64/arm64 CI matrix passed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 10:04:22 +01:00
davideandClaude Sonnet 5.5 4bcbb51605 CI on amd64 and arm64, DEPLOY.md architectures, fix RocksDB teardown in test_storage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 09:52:40 +01:00
davideandClaude Sonnet 5.5 fe02bfec76 README: RocksDB and systemd verified on ARM; CLAUDE.md: always test in a venv
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 09:40:48 +01:00
davideandClaude Sonnet 5.5 dabe76b70d README: ARM status, what is verified and what is left to do
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 12:43:57 +02:00
davideandClaude Sonnet 5.5 e30364c21b Integration tests: SSL service, RocksDB in the CI matrix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:28:37 +02:00
davideandClaude Sonnet 5.5 cba58d7e25 Add a mainnet taproot transaction to the test fixtures
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:28:37 +02:00
davideandClaude Sonnet 5.5 a123bebdd4 Explicit error for broadcast_package, current mainnet sync snapshot, throttling note
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:28:37 +02:00
davideandClaude Sonnet 5.5 fc2672c898 Integration tests: protocol versions, 1.6/1.7 methods, daemon limits and reorg
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:17:43 +02:00
davideandClaude Sonnet 5.5 b32548fd01 Fix mempool.get_info, testmempoolaccept and outpoint.subscribe with the Palladium daemon
- mempool.get_info: the daemon has no incrementalrelayfee in getmempoolinfo,
  read incrementalfee from getnetworkinfo
- testmempoolaccept: the daemon does not report wtxid
- outpoint.subscribe: refuse the subscription (Coin.DAEMON_HAS_TXO_SPENDER_INDEX)
  instead of closing the session at the next block, when the daemon errors

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:17:43 +02:00
15 changed files with 318 additions and 33 deletions

No files matched your search

+24 -9
View File
@@ -14,8 +14,13 @@ concurrency:
jobs: jobs:
unittests: unittests:
name: unittests name: unittests (${{ matrix.os }})
runs-on: ubuntu-24.04 # amd64 and arm64 runners
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -94,8 +99,12 @@ jobs:
docker: docker:
# builds the image and checks the SSL handling of its entrypoint (contrib/docker-smoke-test.sh) # builds the image and checks the SSL handling of its entrypoint (contrib/docker-smoke-test.sh)
name: docker name: docker (${{ matrix.os }})
runs-on: ubuntu-24.04 runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -106,9 +115,14 @@ jobs:
run: contrib/docker-smoke-test.sh plm-electrumx run: contrib/docker-smoke-test.sh plm-electrumx
integration: integration:
# ElectrumX against a real Palladium node in regtest mode (tests/integration) # ElectrumX against a real Palladium node in regtest mode (tests/integration), with each DB engine
name: integration name: integration (${{ matrix.db_engine }}, ${{ matrix.os }})
runs-on: ubuntu-24.04 runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
db_engine: [leveldb, rocksdb]
os: [ubuntu-24.04, ubuntu-24.04-arm]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -121,12 +135,12 @@ jobs:
- name: Install system packages - name: Install system packages
run: | run: |
sudo apt-get -qq update 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 - name: Install Python deps
run: | run: |
pip install pytest pip install pytest
pip install ".[leveldb]" pip install ".[${{ matrix.db_engine }}]"
- name: Start the regtest node - name: Start the regtest node
run: docker compose -f tests/integration/docker-compose.yml up -d --build --wait run: docker compose -f tests/integration/docker-compose.yml up -d --build --wait
@@ -134,6 +148,7 @@ jobs:
- name: Run integration tests - name: Run integration tests
env: env:
PLM_TEST_DAEMON_URL: http://plmtest:plmtest@127.0.0.1:28443/ PLM_TEST_DAEMON_URL: http://plmtest:plmtest@127.0.0.1:28443/
PLM_TEST_DB_ENGINE: ${{ matrix.db_engine }}
run: pytest tests/integration run: pytest tests/integration
- name: Node log - name: Node log
+9 -4
View File
@@ -8,6 +8,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands ## Commands
Always run tests, lint and installs in a Python virtual environment (`python3 -m venv venv && . venv/bin/activate`;
`/venv/` is in `.gitignore`), never with the system pip, also inside Docker containers used to run tests. RocksDB needs
RocksDB >= 8.0 for `rocksdb-ng`: Debian 12 ships 7.8.3, so on it test RocksDB in a `python:3.14-trixie` container
(RocksDB 9.10, set `ALLOW_ROOT=1` there because the server refuses to run as root).
```bash ```bash
pip install -e ".[leveldb,rocksdb]" # at least one DB engine extra is required pip install -e ".[leveldb,rocksdb]" # at least one DB engine extra is required
pip install pytest pytest-asyncio pytest-cov pycodestyle pip install pytest pytest-asyncio pytest-cov pycodestyle
@@ -41,15 +46,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. 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` / `blockchain.transaction.broadcast_package` return a 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. DB schema changed in 2.0 with no migration; changing on-disk format requires a full resync.
## Tests ## 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 ## Repository and workflow
@@ -57,5 +62,5 @@ Integration tests (`tests/integration`): `node/Dockerfile` + `node/palladium.con
- History is a snapshot of upstream commit `280cb33` plus Palladium commits: no common history, `git merge upstream` does not work. Update with `git diff 280cb33 upstream/master | git apply --3way` (see the "Fork notes" in `README.md`). - History is a snapshot of upstream commit `280cb33` plus Palladium commits: no common history, `git merge upstream` does not work. Update with `git diff 280cb33 upstream/master | git apply --3way` (see the "Fork notes" in `README.md`).
- Do not create, move or delete git tags. - Do not create, move or delete git tags.
- Do not `git add -A`: generated files (`.coverage`, `.env`) get swept in. Add files by name. `.coverage` and `.env` are in `.gitignore`. - Do not `git add -A`: generated files (`.coverage`, `.env`) get swept in. Add files by name. `.coverage` and `.env` are in `.gitignore`.
- CI (`.github/workflows/ci.yml`, on `main`): `unittests`, `pycodestyle`, `build_docs`, `docker` (builds the image, runs the smoke test). It runs on Ubuntu 24.04; `test_storage.py` fails with RocksDB 9.10 (Debian trixie) in teardown. - CI (`.github/workflows/ci.yml`, on `main`): `unittests`, `pycodestyle`, `build_docs`, `docker` (builds the image, runs the smoke test). It runs on Ubuntu 24.04, amd64 and arm64 (`ubuntu-24.04-arm`) for `unittests`, `docker` and `integration`.
- In repo documentation never cite paths or links of the palladiumcore repository; describe the concept (the Palladium node, the `palladium` Docker network). - In repo documentation never cite paths or links of the palladiumcore repository; describe the concept (the Palladium node, the `palladium` Docker network).
+9 -1
View File
@@ -189,6 +189,13 @@ DONATION_ADDRESS=plm1q...your address...
amount) and use an address you are happy to make public, since it is sent to everyone who connects. amount) and use an address you are happy to make public, since it is sent to everyone who connects.
- After changing it, apply it with `docker compose up -d` (the container is recreated with the new value). - After changing it, apply it with `docker compose up -d` (the container is recreated with the new value).
## Architectures
amd64 and arm64 (for example a Raspberry Pi 5) are both supported: the Docker image builds natively on either.
Without Docker, the DB engine bindings are compiled from source, so install the development headers first:
`libleveldb-dev` for LevelDB (`plyvel`), or RocksDB >= 8.0 (`librocksdb-dev`) for RocksDB (`rocksdb-ng`).
Debian 12 ships RocksDB 7.8.3, which is too old: use Debian 13 (or the Docker image) for RocksDB.
## Data directory and first start ## Data directory and first start
Create the data directory as the user that runs Docker, before the first start (otherwise Docker creates it as root and Create the data directory as the user that runs Docker, before the first start (otherwise Docker creates it as root and
@@ -238,7 +245,8 @@ From another machine, `nc -zv <public IP> 50001` must succeed if the server is m
| log: connection refused / cannot connect to `palladiumd:2332` | the node is stopped or still starting; check that the node container is running | | log: connection refused / cannot connect to `palladiumd:2332` | the node is stopped or still starting; check that the node container is running |
| log: `401`/authorization errors | wrong `PLM_RPC_USER` / `PLM_RPC_PASSWORD` in `.env`; the credentials must be the ones in the node's `palladium.conf` | | log: `401`/authorization errors | wrong `PLM_RPC_USER` / `PLM_RPC_PASSWORD` in `.env`; the credentials must be the ones in the node's `palladium.conf` |
| errors mentioning `txindex` | `txindex=1` is missing in `palladium.conf` (an index added later is built while the node runs, which takes time) | | errors mentioning `txindex` | `txindex=1` is missing in `palladium.conf` (an index added later is built while the node runs, which takes time) |
| `blockchain.outpoint.subscribe` or package broadcast return a daemon error | expected: Palladium Core does not have the RPCs they need (`gettxspendingprevout`, `submitpackage`) | | `blockchain.outpoint.subscribe` or `blockchain.transaction.broadcast_package` return an error | expected: Palladium Core does not have the RPCs they need (`gettxspendingprevout`, `submitpackage`) |
| a client is disconnected, log says `disconnected whilst throttled` or `closing session over res usage` | expected: the server limits the cost of the requests of each client and IP (for example listing the UTXOs of an address with thousands of outputs); raise `COST_SOFT_LIMIT` / `COST_HARD_LIMIT` for your own trusted clients (see `docs/environment.rst`) |
| the server stays at an old height | the node is not synced; ElectrumX only follows the node | | the server stays at an old height | the node is not synced; ElectrumX only follows the node |
| clients cannot connect from outside | port not published/forwarded, firewall, or `SERVICES` does not list that protocol | | clients cannot connect from outside | port not published/forwarded, firewall, or `SERVICES` does not list that protocol |
| container stops at once, log says `ERROR: ... is not writable` | the data directory belongs to another user (or to root, if Docker created it): `chown -R $(id -u):$(id -g) ~/.plm-electrumx`, or set `PLM_UID`/`PLM_GID` | | container stops at once, log says `ERROR: ... is not writable` | the data directory belongs to another user (or to root, if Docker created it): `chown -R $(id -u):$(id -g) ~/.plm-electrumx`, or set `PLM_UID`/`PLM_GID` |
+6 -2
View File
@@ -16,10 +16,14 @@ RUN apt-get update \
pkg-config \ pkg-config \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# rocksdb-ng is compiled from source (a single C++ translation unit, slow, especially under QEMU for arm64): install it
# in its own layer, before the source is copied, so that changing the code does not recompile it
RUN python -m venv venv \
&& venv/bin/pip install --no-cache-dir rocksdb-ng
COPY pyproject.toml README.md LICENCE MANIFEST.in ./ COPY pyproject.toml README.md LICENCE MANIFEST.in ./
COPY src ./src COPY src ./src
RUN python -m venv venv \ RUN venv/bin/pip install --no-cache-dir ".[rocksdb]"
&& venv/bin/pip install --no-cache-dir ".[rocksdb]"
FROM python:3.14-slim-trixie FROM python:3.14-slim-trixie
+20 -2
View File
@@ -52,6 +52,24 @@ and one of the DB engines (`pip install ".[leveldb]"` or `pip install ".[rocksdb
All settings are environment variables, documented in `docs/environment.rst`; `docs/HOWTO.rst` All settings are environment variables, documented in `docs/environment.rst`; `docs/HOWTO.rst`
describes a full installation. describes a full installation.
## Architectures (amd64 and ARM)
amd64 and arm64 (for example a Raspberry Pi 5) are both supported. The code is pure Python, and the Docker images
(server and integration-test node) pick the right binaries for the architecture (`TARGETARCH`, Debian packages).
The CI runs the `unittests`, `integration` (LevelDB and RocksDB) and `docker` jobs on both architectures
(`ubuntu-24.04` and `ubuntu-24.04-arm`).
Without Docker, the DB engine bindings are compiled from source, so the development headers are needed;
`rocksdb-ng` requires RocksDB >= 8.0 (Debian 13 or the Docker image, not Debian 12). See the "Architectures" section
of `DEPLOY.md`.
To build the server image for both architectures:
docker buildx build --platform linux/amd64,linux/arm64 -t <registry>/plm-electrumx:<tag> --push .
Building the arm64 variant on an amd64 host needs QEMU emulation (`docker run --privileged --rm tonistiigi/binfmt --install arm64`)
and is slow, because `rocksdb-ng` is compiled from source; building on an arm64 host is much faster.
## Fork notes ## Fork notes
### Starting point ### Starting point
@@ -91,8 +109,8 @@ When merging upstream, resolve modify/delete conflicts on these files by keeping
bech32m for taproot) are only read by the local `electrumx_rpc query` command. bech32m for taproot) are only read by the local `electrumx_rpc query` command.
- Palladium Core has no `getindexinfo`, `txospenderindex` (`gettxspendingprevout`) or `submitpackage` RPCs, - Palladium Core has no `getindexinfo`, `txospenderindex` (`gettxspendingprevout`) or `submitpackage` RPCs,
so the daemon version/index checks are disabled for `COIN=Palladium`, and so the daemon version/index checks are disabled for `COIN=Palladium`, and
`blockchain.outpoint.subscribe` and `blockchain.transaction.broadcast_package` return a daemon `blockchain.outpoint.subscribe` is refused with an explicit error (`Coin.DAEMON_HAS_TXO_SPENDER_INDEX`)
error to clients. and `blockchain.transaction.broadcast_package` returns an explicit error to clients.
- The daemon must run with `txindex=1`. - The daemon must run with `txindex=1`.
- Same genesis blocks as Bitcoin (mainnet, testnet3, regtest), 80-byte headers, sha256d PoW. - Same genesis blocks as Bitcoin (mainnet, testnet3, regtest), 80-byte headers, sha256d PoW.
- Parameters (address prefixes, ports, genesis hashes) were taken from the Palladium Core chain parameters. - Parameters (address prefixes, ports, genesis hashes) were taken from the Palladium Core chain parameters.
+9 -3
View File
@@ -13,10 +13,16 @@ First release of the Palladium fork. Based on a snapshot of upstream ElectrumX 2
* Segwit and taproot outputs are supported (the server indexes by script hash); ``Coin.BECH32_HRP`` and a * Segwit and taproot outputs are supported (the server indexes by script hash); ``Coin.BECH32_HRP`` and a
bech32/bech32m decoder let ``electrumx_rpc query`` read ``plm1``, ``tplm1`` and ``rplm1`` addresses. bech32/bech32m decoder let ``electrumx_rpc query`` read ``plm1``, ``tplm1`` and ``rplm1`` addresses.
* Palladium Core has no ``getindexinfo``, ``txospenderindex`` or ``submitpackage`` RPCs: the daemon version/index * Palladium Core has no ``getindexinfo``, ``txospenderindex`` or ``submitpackage`` RPCs: the daemon version/index
checks are disabled, ``txindex=1`` is required, and ``blockchain.outpoint.subscribe`` / checks are disabled, ``txindex=1`` is required, ``blockchain.outpoint.subscribe`` is refused with an explicit
``blockchain.transaction.broadcast_package`` return a daemon error to clients. error (``Coin.DAEMON_HAS_TXO_SPENDER_INDEX``) and ``blockchain.transaction.broadcast_package`` returns an
explicit "not supported" error to clients.
* fixed: ``mempool.get_info`` (protocol 1.6) failed with the Palladium daemon, whose ``getmempoolinfo`` has no
``incrementalrelayfee`` (it is read from ``getnetworkinfo``), and ``blockchain.transaction.testmempoolaccept``
failed because the daemon does not report ``wtxid``.
* new: integration tests (``tests/integration``) against a Palladium node in regtest mode, run in Docker, with * new: integration tests (``tests/integration``) against a Palladium node in regtest mode, run in Docker, with
a ``integration`` job in CI. an ``integration`` job in CI for LevelDB and RocksDB. They cover the protocol versions 1.4 to 1.7, SSL, reorgs and
the daemon limits.
* new: a mainnet taproot transaction among the test fixtures.
* removed: all other coins, their transaction deserializers, session/daemon/block processor classes, tests, * removed: all other coins, their transaction deserializers, session/daemon/block processor classes, tests,
pip extras and ``contrib`` files. pip extras and ``contrib`` files.
* new: ``Dockerfile`` and ``docker-compose.yml`` to run next to the Palladium node container, with automatic * new: ``Dockerfile`` and ``docker-compose.yml`` to run next to the Palladium node container, with automatic
+8 -4
View File
@@ -100,6 +100,8 @@ class Coin:
NET: str | None NET: str | None
MIN_REQUIRED_DAEMON_VERSION: Optional[str] = None MIN_REQUIRED_DAEMON_VERSION: Optional[str] = None
REQUIRED_DAEMON_INDEXES: Sequence[str] = tuple() REQUIRED_DAEMON_INDEXES: Sequence[str] = tuple()
# whether the daemon can tell which tx spends an outpoint (gettxspendingprevout); needed to subscribe to outpoints
DAEMON_HAS_TXO_SPENDER_INDEX = True
# only used for initial db sync ETAs: # only used for initial db sync ETAs:
TX_COUNT_HEIGHT: int # at a given snapshot of the chain, TX_COUNT_HEIGHT: int # at a given snapshot of the chain,
@@ -268,8 +270,9 @@ class Palladium(Coin):
so no special handling is needed for either). so no special handling is needed for either).
Its daemon has no 'txospenderindex', 'getindexinfo' or 'submitpackage', so Its daemon has no 'txospenderindex', 'getindexinfo' or 'submitpackage', so
'blockchain.outpoint.subscribe' and 'blockchain.transaction.broadcast_package' will 'blockchain.outpoint.subscribe' is refused (DAEMON_HAS_TXO_SPENDER_INDEX) and
return daemon errors to clients. The daemon needs 'txindex=1'. 'blockchain.transaction.broadcast_package' returns an explicit error to clients.
The daemon needs 'txindex=1'.
""" """
NAME = "Palladium" NAME = "Palladium"
SHORTNAME = "PLM" SHORTNAME = "PLM"
@@ -282,10 +285,11 @@ class Palladium(Coin):
'4ff763ae46a2a6c172b3f1b60a8ce26f') '4ff763ae46a2a6c172b3f1b60a8ce26f')
MEMPOOL_HISTOGRAM_REFRESH_SECS = 120 MEMPOOL_HISTOGRAM_REFRESH_SECS = 120
DESERIALIZER = lib_tx.DeserializerSegWit DESERIALIZER = lib_tx.DeserializerSegWit
DAEMON_HAS_TXO_SPENDER_INDEX = False
RPC_PORT = 2332 RPC_PORT = 2332
# snapshot (UpdateTip tx= count), only used for sync ETAs # snapshot (UpdateTip tx= count), only used for sync ETAs
TX_COUNT = 661_421 TX_COUNT = 662_327
TX_COUNT_HEIGHT = 542_032 TX_COUNT_HEIGHT = 542_786
TX_PER_BLOCK = 2 TX_PER_BLOCK = 2
PEERS = [] PEERS = []
+5 -1
View File
@@ -336,6 +336,10 @@ class Daemon:
async def mempool_info(self) -> dict[str, float]: async def mempool_info(self) -> dict[str, float]:
mempool_info = await self.getmempoolinfo() mempool_info = await self.getmempoolinfo()
incrementalrelayfee = mempool_info.get('incrementalrelayfee')
if incrementalrelayfee is None:
# the Palladium daemon only reports it in getnetworkinfo, as 'incrementalfee'
incrementalrelayfee = (await self.getnetworkinfo())['incrementalfee']
return { return {
# Dynamic minimum fee rate in PLM/kvB for tx to be accepted. # Dynamic minimum fee rate in PLM/kvB for tx to be accepted.
# Is the maximum of minrelaytxfee and minimum mempool fee # Is the maximum of minrelaytxfee and minimum mempool fee
@@ -344,7 +348,7 @@ class Daemon:
'minrelaytxfee': mempool_info['minrelaytxfee'], 'minrelaytxfee': mempool_info['minrelaytxfee'],
# Minimum fee rate increment for mempool limiting or replacement in PLM/kvB, # Minimum fee rate increment for mempool limiting or replacement in PLM/kvB,
# operator-configurable, static. # operator-configurable, static.
'incrementalrelayfee': mempool_info['incrementalrelayfee'], 'incrementalrelayfee': incrementalrelayfee,
} }
async def getrawtransaction(self, txid_hum: str, verbose=False): async def getrawtransaction(self, txid_hum: str, verbose=False):
+10 -4
View File
@@ -1876,6 +1876,11 @@ class ElectrumX(SessionBase):
spk_hint: scriptPubKey corresponding to the outpoint. Might be used by spk_hint: scriptPubKey corresponding to the outpoint. Might be used by
other servers, but we don't need and hence ignore it. other servers, but we don't need and hence ignore it.
''' '''
if not self.env.coin.DAEMON_HAS_TXO_SPENDER_INDEX:
# the status of a subscribed outpoint is re-checked with the daemon at every block: without
# support the subscription would be accepted now and then close the session
raise RPCError(BAD_REQUEST, 'outpoint subscriptions are not supported: the daemon cannot look up '
'the spender of an outpoint')
txid_rev = assert_txid_hum(tx_hash) txid_rev = assert_txid_hum(tx_hash)
txout_idx = non_negative_integer(txout_idx) txout_idx = non_negative_integer(txout_idx)
assert_hex_str(spk_hint) assert_hex_str(spk_hint)
@@ -2235,6 +2240,8 @@ class ElectrumX(SessionBase):
error, = e.args error, = e.args
message = error['message'] message = error['message']
self.logger.info(f"error submitting package: {message}") self.logger.info(f"error submitting package: {message}")
if error.get('code') == -32601: # method not found
raise RPCError(BAD_REQUEST, 'the daemon does not support package submission (submitpackage)')
raise RPCError( raise RPCError(
BAD_REQUEST, BAD_REQUEST,
f'the tx package was rejected by network rules.\n\n{message}.', f'the tx package was rejected by network rules.\n\n{message}.',
@@ -2273,10 +2280,9 @@ class ElectrumX(SessionBase):
response: list[dict] = [] response: list[dict] = []
for orig_item in daemon_result: # one item for each tx for orig_item in daemon_result: # one item for each tx
new_item = { new_item = {"txid": orig_item["txid"]}
"txid": orig_item["txid"], if "wtxid" in orig_item: # not reported by older daemons (Palladium Core)
"wtxid": orig_item["wtxid"], new_item["wtxid"] = orig_item["wtxid"]
}
# optional: "allowed" field # optional: "allowed" field
if orig_item.get("allowed") in (True, False): if orig_item.get("allowed") in (True, False):
new_item["allowed"] = orig_item["allowed"] new_item["allowed"] = orig_item["allowed"]
+17 -2
View File
@@ -11,6 +11,7 @@ import base64
import json import json
import os import os
import socket import socket
import ssl
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@@ -100,8 +101,10 @@ def scripthash(node, address):
class ElectrumClient: class ElectrumClient:
'''Newline-delimited JSON-RPC over TCP, one request at a time (notifications are skipped).''' '''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) 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.file = self.sock.makefile('rwb')
self.next_id = 0 self.next_id = 0
@@ -122,6 +125,10 @@ class ElectrumClient:
self.sock.close() 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(): def free_port():
with socket.socket() as s: with socket.socket() as s:
s.bind(('127.0.0.1', 0)) s.bind(('127.0.0.1', 0))
@@ -136,11 +143,19 @@ def electrumx(node, wallet):
if node.call('getblockcount') < MIN_HEIGHT: if node.call('getblockcount') < MIN_HEIGHT:
node.mine(MIN_HEIGHT - node.call('getblockcount'), miner) node.mine(MIN_HEIGHT - node.call('getblockcount'), miner)
port = free_port() port = free_port()
ssl_port = free_port()
with tempfile.TemporaryDirectory() as db_dir: 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, env = dict(os.environ,
COIN='Palladium', NET='regtest', COIN='Palladium', NET='regtest',
DB_ENGINE=os.environ.get('PLM_TEST_DB_ENGINE', 'leveldb'), DB_DIRECTORY=db_dir, 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') PEER_DISCOVERY='off', PEER_ANNOUNCE='', LOG_LEVEL='info')
log = open(os.path.join(db_dir, 'electrumx.log'), 'w+') 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, proc = subprocess.Popen([sys.executable, str(REPO_ROOT / 'electrumx_server')], cwd=REPO_ROOT, env=env,
+134
View File
@@ -0,0 +1,134 @@
'''Electrum protocol methods per protocol version, daemon limits and reorgs, against the regtest node (see conftest.py).'''
import pytest
import ssl
from tests.integration.conftest import SSL_SERVICE, ElectrumClient, new_address, scripthash, wait_for
@pytest.fixture
def client(electrumx):
client = ElectrumClient(electrumx)
yield client
client.close()
def negotiate(client, version):
return client.call('server.version', 'plm-integration-test', version)
def spk_of(node, address):
return node.call('getaddressinfo', address)['scriptPubKey']
@pytest.mark.parametrize('version', ['1.4', '1.4.2', '1.5', '1.6', '1.7'])
def test_protocol_versions(client, version):
assert negotiate(client, version)[1] == version
def test_unsupported_protocol_versions(electrumx):
for version in ('1.3', '2.0'):
client = ElectrumClient(electrumx)
try:
with pytest.raises(RuntimeError, match='unsupported protocol version'):
negotiate(client, version)
finally:
client.close()
def test_server_features(node, client):
negotiate(client, '1.4')
features = client.call('server.features')
assert features['genesis_hash'] == node.call('getblockhash', 0)
assert features['hash_function'] == 'sha256'
assert features['protocol_min'] == '1.4'
def test_relayfee_and_estimatefee(client):
negotiate(client, '1.5') # blockchain.relayfee was removed in 1.6
assert client.call('blockchain.relayfee') > 0
assert isinstance(client.call('blockchain.estimatefee', 2), (int, float))
def test_mempool_info(client):
'''The daemon reports 'incrementalfee' in getnetworkinfo, not 'incrementalrelayfee' in getmempoolinfo'''
negotiate(client, '1.6')
info = client.call('mempool.get_info')
assert set(info) == {'mempoolminfee', 'minrelaytxfee', 'incrementalrelayfee'}
assert all(fee > 0 for fee in info.values())
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='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'):
client.call('blockchain.outpoint.subscribe', '00' * 32, 0, '')
def test_protocol_1_7_scriptpubkey_methods(node, wallet, client):
negotiate(client, '1.7')
target = node.call('getnewaddress', '', 'bech32m', wallet=wallet)
spk = spk_of(node, target)
txid = node.call('sendtoaddress', target, 2, wallet=wallet)
wait_for(lambda: client.call('blockchain.scriptpubkey.get_mempool', spk)['history'], 'the tx in the mempool')
assert txid in [h['tx_hash'] for h in client.call('blockchain.scriptpubkey.get_history', spk)['history']]
assert client.call('blockchain.scriptpubkey.get_balance', spk)['unconfirmed'] == 200_000_000
assert client.call('blockchain.scriptpubkey.subscribe', spk) is not None
# mempool.recent lists it
assert txid in [t['txid'] for t in client.call('mempool.recent')]
node.mine(1, new_address(node, wallet))
wait_for(lambda: client.call('blockchain.scriptpubkey.get_balance', spk)['confirmed'] == 200_000_000, 'confirmation')
unspent = client.call('blockchain.scriptpubkey.listunspent', spk)['utxos']
assert [(u['tx_hash'], u['value']) for u in unspent] == [(txid, 200_000_000)]
assert client.call('blockchain.scriptpubkey.unsubscribe', spk) is True
def test_testmempoolaccept(node, wallet, client):
negotiate(client, '1.7')
target = new_address(node, wallet)
funded = node.call('fundrawtransaction', node.call('createrawtransaction', [], {target: 0.1}), wallet=wallet)
signed = node.call('signrawtransactionwithwallet', funded['hex'], wallet=wallet)['hex']
result, = client.call('blockchain.transaction.testmempoolaccept', [signed])
assert result['allowed'] is True
def test_reorg_replaces_blocks(node, wallet, electrumx, client):
'''The server follows a reorg: the block is replaced by a longer chain that confirms the tx again.
Like upstream ElectrumX it reorganises when the daemon's chain gets longer than its own, not before.'''
negotiate(client, '1.4.2')
miner = new_address(node, wallet)
target = node.call('getnewaddress', '', 'bech32', wallet=wallet)
sh = scripthash(node, target)
txid = node.call('sendtoaddress', target, 1, wallet=wallet)
block = node.mine(1, miner)[0]
height = node.call('getblockcount')
wait_for(lambda: client.call('blockchain.scripthash.get_balance', sh)['confirmed'] == 100_000_000, 'confirmation')
old_header = client.call('blockchain.block.header', height)
node.call('invalidateblock', block)
new_blocks = node.mine(2, new_address(node, wallet)) # other coinbase, else the same block; the tx goes back to the mempool and is mined again in the first one
assert block not in new_blocks
wait_for(lambda: client.call('blockchain.headers.subscribe')['height'] == height + 1, 'the server to follow the new chain')
assert client.call('blockchain.block.header', height) != old_header
wait_for(lambda: client.call('blockchain.scripthash.get_balance', sh) == {'confirmed': 100_000_000, 'unconfirmed': 0},
'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())
+1
View File
@@ -46,6 +46,7 @@ def test_daemon_requirements_disabled():
for coin, *_ in PARAMS: for coin, *_ in PARAMS:
assert coin.MIN_REQUIRED_DAEMON_VERSION is None assert coin.MIN_REQUIRED_DAEMON_VERSION is None
assert not coin.REQUIRED_DAEMON_INDEXES assert not coin.REQUIRED_DAEMON_INDEXES
assert coin.DAEMON_HAS_TXO_SPENDER_INDEX is False
def bech32_encode_v0(hrp, program): def bech32_encode_v0(hrp, program):
+11
View File
@@ -261,6 +261,17 @@ async def test_mempool_info(daemon):
assert await daemon.mempool_info() == expected_result assert await daemon.mempool_info() == expected_result
@pytest.mark.asyncio
async def test_mempool_info_incrementalfee_from_networkinfo(daemon):
# Palladium Core: getmempoolinfo has no 'incrementalrelayfee', getnetworkinfo has 'incrementalfee'
daemon.session = ClientSessionGood(
('getmempoolinfo', [], {'mempoolminfee': 0.00001, 'minrelaytxfee': 0.00002, 'other': 'cruft'}),
('getnetworkinfo', [], {'incrementalfee': 0.00003, 'other': 'cruft'}),
)
assert await daemon.mempool_info() == {
'mempoolminfee': 0.00001, 'minrelaytxfee': 0.00002, 'incrementalrelayfee': 0.00003}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mempool_txids_hum(daemon): async def test_mempool_txids_hum(daemon):
hashes = ['hex_hash1', 'hex_hash2'] hashes = ['hex_hash1', 'hex_hash2']
+1 -1
View File
@@ -22,8 +22,8 @@ def db(tmpdir, request):
os.chdir(str(tmpdir)) os.chdir(str(tmpdir))
db = db_class(request.param)("db", False) db = db_class(request.param)("db", False)
yield db yield db
os.chdir(cwd)
db.close() db.close()
os.chdir(cwd)
def test_put_get(db): def test_put_get(db):
@@ -0,0 +1,54 @@
{
"in_active_chain": true,
"txid": "2a74f4044c5f756ddd286c9d355e1754786517d95417b79a2e12e744d1ae37c6",
"hash": "633018454b4f57e7dd164c5199d56b56cdccd501ac8ba82ae8b29497f7651eaa",
"version": 2,
"size": 246,
"vsize": 165,
"weight": 657,
"locktime": 453779,
"vin": [
{
"txid": "6e3b6929555d05ebed7aa4694e23bb3c07b2a258e25cf8c76b7b6a67bece58a8",
"vout": 1,
"scriptSig": {
"asm": "",
"hex": ""
},
"txinwitness": [
"304402202c27dcea17fbc79e808df00bc39483299c9ae1d0f8d1d4d10a6a660121348d2d02206d605216441034a9ea0836e7aadf1aa34892850b04f3a74c80ad36341d03f95201",
"03a5e365f5f47644266ffbb627cb4ca3ec7aa920c8720b0b926cd14646682bc4ed"
],
"sequence": 4294967293
}
],
"vout": [
{
"value": 1152511,
"n": 0,
"scriptPubKey": {
"asm": "1 131b356f279830e10aff3af2f7ef0739bbcdc71bfb4703fa4600e320f35793ab",
"hex": "5120131b356f279830e10aff3af2f7ef0739bbcdc71bfb4703fa4600e320f35793ab",
"reqSigs": 1,
"type": "witness_v1_taproot",
"addresses": [
"plm1pzvdn2me8nqcwzzhl8te00mc88xaum3cmldrs87jxqr3jpu6hjw4sqnd9um"
]
}
},
{
"value": 100000,
"n": 1,
"scriptPubKey": {
"asm": "1 074e5a915545d57d8d66c68187be874779aa1cfdad2987ede47ab63326c6e1a9",
"hex": "5120074e5a915545d57d8d66c68187be874779aa1cfdad2987ede47ab63326c6e1a9",
"reqSigs": 1,
"type": "witness_v1_taproot",
"addresses": [
"plm1pqa894y24gh2hmrtxc6qc0058gau6588a455c0m0y02mrxfkxux5su66t93"
]
}
}
],
"hex": "02000000000101a858cebe676a7b6bc7f85ce258a2b2073cbb234e69a47aedeb055d5529693b6e0100000000fdffffff02ff95110000000000225120131b356f279830e10aff3af2f7ef0739bbcdc71bfb4703fa4600e320f35793aba086010000000000225120074e5a915545d57d8d66c68187be874779aa1cfdad2987ede47ab63326c6e1a90247304402202c27dcea17fbc79e808df00bc39483299c9ae1d0f8d1d4d10a6a660121348d2d02206d605216441034a9ea0836e7aadf1aa34892850b04f3a74c80ad36341d03f952012103a5e365f5f47644266ffbb627cb4ca3ec7aa920c8720b0b926cd14646682bc4ed93ec0600"
}