Compare commits
12
Commits
27ef7cbabf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37d67385f4 | ||
|
|
be689666cd | ||
|
|
b53f9fdf5b | ||
|
|
82b864982d | ||
|
|
4bcbb51605 | ||
|
|
fe02bfec76 | ||
|
|
dabe76b70d | ||
|
|
e30364c21b | ||
|
|
cba58d7e25 | ||
|
|
a123bebdd4 | ||
|
|
fc2672c898 | ||
|
|
b32548fd01 |
No files matched your search
@@ -14,8 +14,13 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
unittests:
|
||||
name: unittests
|
||||
runs-on: ubuntu-24.04
|
||||
name: unittests (${{ matrix.os }})
|
||||
# amd64 and arm64 runners
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -94,8 +99,12 @@ jobs:
|
||||
|
||||
docker:
|
||||
# builds the image and checks the SSL handling of its entrypoint (contrib/docker-smoke-test.sh)
|
||||
name: docker
|
||||
runs-on: ubuntu-24.04
|
||||
name: docker (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -106,9 +115,14 @@ jobs:
|
||||
run: contrib/docker-smoke-test.sh plm-electrumx
|
||||
|
||||
integration:
|
||||
# ElectrumX against a real Palladium node in regtest mode (tests/integration)
|
||||
name: integration
|
||||
runs-on: ubuntu-24.04
|
||||
# ElectrumX against a real Palladium node in regtest mode (tests/integration), with each DB engine
|
||||
name: integration (${{ matrix.db_engine }}, ${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
db_engine: [leveldb, rocksdb]
|
||||
os: [ubuntu-24.04, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -121,12 +135,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 +148,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
|
||||
|
||||
@@ -8,6 +8,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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
|
||||
pip install -e ".[leveldb,rocksdb]" # at least one DB engine extra is required
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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`).
|
||||
- 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`.
|
||||
- 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).
|
||||
@@ -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.
|
||||
- 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
|
||||
|
||||
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: `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) |
|
||||
| `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 |
|
||||
| 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` |
|
||||
|
||||
+6
-2
@@ -16,10 +16,14 @@ RUN apt-get update \
|
||||
pkg-config \
|
||||
&& 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 src ./src
|
||||
RUN python -m venv venv \
|
||||
&& venv/bin/pip install --no-cache-dir ".[rocksdb]"
|
||||
RUN venv/bin/pip install --no-cache-dir ".[rocksdb]"
|
||||
|
||||
|
||||
FROM python:3.14-slim-trixie
|
||||
|
||||
@@ -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`
|
||||
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
|
||||
|
||||
### 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.
|
||||
- Palladium Core has no `getindexinfo`, `txospenderindex` (`gettxspendingprevout`) or `submitpackage` RPCs,
|
||||
so the daemon version/index checks are disabled for `COIN=Palladium`, and
|
||||
`blockchain.outpoint.subscribe` and `blockchain.transaction.broadcast_package` return a daemon
|
||||
error to clients.
|
||||
`blockchain.outpoint.subscribe` is refused with an explicit error (`Coin.DAEMON_HAS_TXO_SPENDER_INDEX`)
|
||||
and `blockchain.transaction.broadcast_package` returns an explicit error to clients.
|
||||
- The daemon must run with `txindex=1`.
|
||||
- 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.
|
||||
|
||||
+9
-3
@@ -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
|
||||
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
|
||||
checks are disabled, ``txindex=1`` is required, and ``blockchain.outpoint.subscribe`` /
|
||||
``blockchain.transaction.broadcast_package`` return a daemon error to clients.
|
||||
checks are disabled, ``txindex=1`` is required, ``blockchain.outpoint.subscribe`` is refused with an explicit
|
||||
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
|
||||
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,
|
||||
pip extras and ``contrib`` files.
|
||||
* new: ``Dockerfile`` and ``docker-compose.yml`` to run next to the Palladium node container, with automatic
|
||||
|
||||
@@ -100,6 +100,8 @@ class Coin:
|
||||
NET: str | None
|
||||
MIN_REQUIRED_DAEMON_VERSION: Optional[str] = None
|
||||
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:
|
||||
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).
|
||||
|
||||
Its daemon has no 'txospenderindex', 'getindexinfo' or 'submitpackage', so
|
||||
'blockchain.outpoint.subscribe' and 'blockchain.transaction.broadcast_package' will
|
||||
return daemon errors to clients. The daemon needs 'txindex=1'.
|
||||
'blockchain.outpoint.subscribe' is refused (DAEMON_HAS_TXO_SPENDER_INDEX) and
|
||||
'blockchain.transaction.broadcast_package' returns an explicit error to clients.
|
||||
The daemon needs 'txindex=1'.
|
||||
"""
|
||||
NAME = "Palladium"
|
||||
SHORTNAME = "PLM"
|
||||
@@ -282,10 +285,11 @@ class Palladium(Coin):
|
||||
'4ff763ae46a2a6c172b3f1b60a8ce26f')
|
||||
MEMPOOL_HISTOGRAM_REFRESH_SECS = 120
|
||||
DESERIALIZER = lib_tx.DeserializerSegWit
|
||||
DAEMON_HAS_TXO_SPENDER_INDEX = False
|
||||
RPC_PORT = 2332
|
||||
# snapshot (UpdateTip tx= count), only used for sync ETAs
|
||||
TX_COUNT = 661_421
|
||||
TX_COUNT_HEIGHT = 542_032
|
||||
TX_COUNT = 662_327
|
||||
TX_COUNT_HEIGHT = 542_786
|
||||
TX_PER_BLOCK = 2
|
||||
PEERS = []
|
||||
|
||||
|
||||
@@ -336,6 +336,10 @@ class Daemon:
|
||||
|
||||
async def mempool_info(self) -> dict[str, float]:
|
||||
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 {
|
||||
# Dynamic minimum fee rate in PLM/kvB for tx to be accepted.
|
||||
# Is the maximum of minrelaytxfee and minimum mempool fee
|
||||
@@ -344,7 +348,7 @@ class Daemon:
|
||||
'minrelaytxfee': mempool_info['minrelaytxfee'],
|
||||
# Minimum fee rate increment for mempool limiting or replacement in PLM/kvB,
|
||||
# operator-configurable, static.
|
||||
'incrementalrelayfee': mempool_info['incrementalrelayfee'],
|
||||
'incrementalrelayfee': incrementalrelayfee,
|
||||
}
|
||||
|
||||
async def getrawtransaction(self, txid_hum: str, verbose=False):
|
||||
|
||||
@@ -1876,6 +1876,11 @@ class ElectrumX(SessionBase):
|
||||
spk_hint: scriptPubKey corresponding to the outpoint. Might be used by
|
||||
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)
|
||||
txout_idx = non_negative_integer(txout_idx)
|
||||
assert_hex_str(spk_hint)
|
||||
@@ -2235,6 +2240,8 @@ class ElectrumX(SessionBase):
|
||||
error, = e.args
|
||||
message = error['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(
|
||||
BAD_REQUEST,
|
||||
f'the tx package was rejected by network rules.\n\n{message}.',
|
||||
@@ -2273,10 +2280,9 @@ class ElectrumX(SessionBase):
|
||||
|
||||
response: list[dict] = []
|
||||
for orig_item in daemon_result: # one item for each tx
|
||||
new_item = {
|
||||
"txid": orig_item["txid"],
|
||||
"wtxid": orig_item["wtxid"],
|
||||
}
|
||||
new_item = {"txid": orig_item["txid"]}
|
||||
if "wtxid" in orig_item: # not reported by older daemons (Palladium Core)
|
||||
new_item["wtxid"] = orig_item["wtxid"]
|
||||
# optional: "allowed" field
|
||||
if orig_item.get("allowed") in (True, False):
|
||||
new_item["allowed"] = orig_item["allowed"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
@@ -46,6 +46,7 @@ def test_daemon_requirements_disabled():
|
||||
for coin, *_ in PARAMS:
|
||||
assert coin.MIN_REQUIRED_DAEMON_VERSION is None
|
||||
assert not coin.REQUIRED_DAEMON_INDEXES
|
||||
assert coin.DAEMON_HAS_TXO_SPENDER_INDEX is False
|
||||
|
||||
|
||||
def bech32_encode_v0(hrp, program):
|
||||
|
||||
@@ -261,6 +261,17 @@ async def test_mempool_info(daemon):
|
||||
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
|
||||
async def test_mempool_txids_hum(daemon):
|
||||
hashes = ['hex_hash1', 'hex_hash2']
|
||||
|
||||
@@ -22,8 +22,8 @@ def db(tmpdir, request):
|
||||
os.chdir(str(tmpdir))
|
||||
db = db_class(request.param)("db", False)
|
||||
yield db
|
||||
os.chdir(cwd)
|
||||
db.close()
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
Reference in new issue
Block a user