diff --git a/Makefile.am b/Makefile.am
index 8b01e680..00490a40 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -49,6 +49,7 @@ src_libbitcoin_server_la_SOURCES = \
src/protocols/protocol_http.cpp \
src/protocols/bitcoind/protocol_bitcoind_rest.cpp \
src/protocols/bitcoind/protocol_bitcoind_rpc.cpp \
+ src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp \
src/protocols/electrum/protocol_electrum.cpp \
src/protocols/electrum/protocol_electrum_addresses.cpp \
src/protocols/electrum/protocol_electrum_fees.cpp \
@@ -93,6 +94,7 @@ test_libbitcoin_server_test_SOURCES = \
test/parsers/electrum_version.cpp \
test/parsers/native_query.cpp \
test/parsers/native_target.cpp \
+ test/protocols/bitcoind/bitcoind_json.cpp \
test/protocols/bitcoind/bitcoind_rest.cpp \
test/protocols/bitcoind/bitcoind_rpc.cpp \
test/protocols/bitcoind/bitcoind_setup_fixture.cpp \
diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
index 01210daa..14be6f87 100644
--- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
@@ -131,6 +131,7 @@
+
diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters
index 47a68fa3..1c804ffb 100644
--- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters
@@ -57,6 +57,9 @@
src\parsers
+
+ src\protocols\bitcoind
+
src\protocols\bitcoind
diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj
index 01aa5cde..e95d1d4a 100644
--- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj
@@ -131,6 +131,7 @@
+
diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters
index 203758f9..994d6cc2 100644
--- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters
@@ -93,6 +93,9 @@
src\protocols\bitcoind
+
+ src\protocols\bitcoind
+
src\protocols\electrum
diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj
index 3e8d017a..3861753f 100644
--- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj
@@ -131,6 +131,7 @@
+
diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters
index 47a68fa3..1c804ffb 100644
--- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters
@@ -57,6 +57,9 @@
src\parsers
+
+ src\protocols\bitcoind
+
src\protocols\bitcoind
diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj
index ea9667da..e44cfc13 100644
--- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj
@@ -131,6 +131,7 @@
+
diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters
index 203758f9..994d6cc2 100644
--- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters
@@ -93,6 +93,9 @@
src\protocols\bitcoind
+
+ src\protocols\bitcoind
+
src\protocols\electrum
diff --git a/endpoints/test_bitcoind_rest.py b/endpoints/test_bitcoind_rest.py
new file mode 100644
index 00000000..c82475bb
--- /dev/null
+++ b/endpoints/test_bitcoind_rest.py
@@ -0,0 +1,257 @@
+"""
+Tests for libbitcoin-server bitcoind REST compatibility interface.
+
+Covers the Bitcoin Core REST endpoints served under /rest/: chaininfo, block,
+block/notxdetails, block/spent (libbitcoin extension), blockhashbyheight,
+headers, blockpart (libbitcoin extension), and the basic (neutrino) filters.
+
+Per-endpoint media type is selected by the URL extension: .json, .hex (text),
+or .bin (raw bytes). doc:
+ github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md
+
+Run with:
+ pytest test_bitcoind_rest.py
+ pytest test_bitcoind_rest.py --bitcoind-rest-host=localhost --bitcoind-rest-port=8332
+"""
+
+import hashlib
+import os
+import time
+import pytest
+import requests
+
+from utils import ReferenceData, TestConfig, reverse_hex, double_sha256
+
+
+HEADER_SIZE = 80 # serialized block header size, in bytes
+
+
+def _header_hash(header: bytes) -> str:
+ """Block hash (display byte order) from the first 80 bytes of a header."""
+ digest = hashlib.sha256(hashlib.sha256(header[:HEADER_SIZE]).digest()).hexdigest()
+ return reverse_hex(digest)
+
+
+def _get(rest_config: dict, path: str) -> requests.Response:
+ """GET a REST path. A non-200 status degrades to xfail (as in the RPC suite)."""
+ url = f"{rest_config['base_url']}/{path}"
+
+ if os.getenv("BITCOIND_DEBUG"):
+ print(f">>> GET {url}", flush=True)
+
+ _t0 = time.monotonic()
+ try:
+ resp = requests.get(
+ url,
+ timeout=TestConfig.DEFAULT_RPC_TIMEOUT,
+ headers={"Connection": "close"}
+ )
+ except requests.exceptions.RequestException as e:
+ raise RuntimeError(f"REST connection error: {e}")
+ _elapsed = time.monotonic() - _t0
+
+ if os.getenv("BITCOIND_DEBUG"):
+ print(f"<<< {path} ({_elapsed * 1000:.1f} ms) "
+ f"status={resp.status_code} bytes={len(resp.content)}", flush=True)
+
+ if resp.status_code != 200:
+ pytest.xfail(f"Server sent status {resp.status_code} for {path}: "
+ f"{resp.text[:200]!r}")
+
+ return resp
+
+
+def get_json(rest_config: dict, path: str):
+ resp = _get(rest_config, path)
+ try:
+ return resp.json()
+ except ValueError:
+ pytest.fail(f"Non-JSON response for {path}: {resp.text[:200]!r}")
+
+
+def get_hex(rest_config: dict, path: str) -> str:
+ return _get(rest_config, path).text.strip()
+
+
+def get_bin(rest_config: dict, path: str) -> bytes:
+ return _get(rest_config, path).content
+
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# CHAIN INFORMATION
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_chaininfo_json(bitcoind_rest_config):
+ """GET /rest/chaininfo.json returns blockchain state."""
+ data = get_json(bitcoind_rest_config, "chaininfo.json")
+
+ assert isinstance(data, dict)
+ assert data.get("chain") == "main"
+ for field in ("blocks", "headers", "bestblockhash", "difficulty"):
+ assert field in data
+ assert len(data["bestblockhash"]) == 64
+
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# BLOCK (full, notxdetails, spent)
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_block_json(bitcoind_rest_config):
+ """GET /rest/block/.json returns a block with full tx objects."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"block/{ReferenceData.KNOWN_BLOCK_HASH}.json"
+ )
+
+ assert isinstance(data, dict)
+ assert data["hash"] == ReferenceData.KNOWN_BLOCK_HASH
+ assert isinstance(data["tx"], list) and len(data["tx"]) >= 1
+ assert isinstance(data["tx"][0], dict) # full transaction objects
+
+
+def test_block_hex(bitcoind_rest_config):
+ """GET /rest/block/.hex returns the raw block; header hashes to ."""
+ raw = get_hex(bitcoind_rest_config, f"block/{ReferenceData.KNOWN_BLOCK_HASH}.hex")
+
+ assert len(raw) >= 2 * HEADER_SIZE
+ assert all(c in "0123456789abcdefABCDEF" for c in raw)
+ assert reverse_hex(double_sha256(raw[:2 * HEADER_SIZE])) == ReferenceData.KNOWN_BLOCK_HASH
+
+
+def test_block_bin(bitcoind_rest_config):
+ """GET /rest/block/.bin returns raw bytes; header hashes to ."""
+ raw = get_bin(bitcoind_rest_config, f"block/{ReferenceData.KNOWN_BLOCK_HASH}.bin")
+
+ assert len(raw) >= HEADER_SIZE
+ assert _header_hash(raw) == ReferenceData.KNOWN_BLOCK_HASH
+
+
+def test_block_notxdetails_json(bitcoind_rest_config):
+ """GET /rest/block/notxdetails/.json lists txids, not full objects."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"block/notxdetails/{ReferenceData.KNOWN_BLOCK_HASH}.json"
+ )
+
+ assert isinstance(data, dict)
+ assert data["hash"] == ReferenceData.KNOWN_BLOCK_HASH
+ assert isinstance(data["tx"], list) and len(data["tx"]) >= 1
+ assert isinstance(data["tx"][0], str)
+ assert len(data["tx"][0]) == 64
+
+
+def test_block_spent_json(bitcoind_rest_config):
+ """GET /rest/block/spent/.json (libbitcoin extension) returns spent outputs."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"block/spent/{ReferenceData.KNOWN_BLOCK_HASH}.json"
+ )
+
+ assert isinstance(data, (list, dict))
+
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# BLOCK HASH BY HEIGHT
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_blockhashbyheight_json(bitcoind_rest_config):
+ """GET /rest/blockhashbyheight/.json returns the block hash."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"blockhashbyheight/{ReferenceData.KNOWN_HEIGHT}.json"
+ )
+
+ assert isinstance(data, dict)
+ assert data["blockhash"] == ReferenceData.KNOWN_BLOCK_HASH
+
+
+def test_blockhashbyheight_genesis_json(bitcoind_rest_config):
+ """GET /rest/blockhashbyheight/0.json returns the genesis hash."""
+ data = get_json(bitcoind_rest_config, "blockhashbyheight/0.json")
+
+ assert data["blockhash"] == ReferenceData.GENESIS_HASH
+
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# HEADERS
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_headers_json(bitcoind_rest_config):
+ """GET /rest/headers//.json returns header objects."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"headers/5/{ReferenceData.KNOWN_BLOCK_HASH}.json"
+ )
+
+ assert isinstance(data, list)
+ assert len(data) == 5
+ assert data[0]["hash"] == ReferenceData.KNOWN_BLOCK_HASH
+ for field in ("merkleroot", "time", "nonce", "bits"):
+ assert field in data[0]
+
+
+def test_headers_hex(bitcoind_rest_config):
+ """GET /rest/headers/1/.hex returns one 80-byte header hashing to ."""
+ raw = get_hex(bitcoind_rest_config, f"headers/1/{ReferenceData.KNOWN_BLOCK_HASH}.hex")
+
+ assert len(raw) == 2 * HEADER_SIZE
+ assert reverse_hex(double_sha256(raw)) == ReferenceData.KNOWN_BLOCK_HASH
+
+
+def test_headers_bin(bitcoind_rest_config):
+ """GET /rest/headers/2/.bin returns two concatenated 80-byte headers."""
+ raw = get_bin(bitcoind_rest_config, f"headers/2/{ReferenceData.KNOWN_BLOCK_HASH}.bin")
+
+ assert len(raw) == 2 * HEADER_SIZE
+ assert _header_hash(raw[:HEADER_SIZE]) == ReferenceData.KNOWN_BLOCK_HASH
+
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# BLOCK PART (libbitcoin extension, bin|hex only)
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_blockpart_hex_header(bitcoind_rest_config):
+ """GET /rest/blockpart//0/80.hex returns the block's 80-byte header."""
+ raw = get_hex(
+ bitcoind_rest_config,
+ f"blockpart/{ReferenceData.KNOWN_BLOCK_HASH}/0/{HEADER_SIZE}.hex"
+ )
+
+ assert len(raw) == 2 * HEADER_SIZE
+ assert reverse_hex(double_sha256(raw)) == ReferenceData.KNOWN_BLOCK_HASH
+
+
+def test_blockpart_bin_header(bitcoind_rest_config):
+ """GET /rest/blockpart//0/80.bin returns the block's 80-byte header."""
+ raw = get_bin(
+ bitcoind_rest_config,
+ f"blockpart/{ReferenceData.KNOWN_BLOCK_HASH}/0/{HEADER_SIZE}.bin"
+ )
+
+ assert len(raw) == HEADER_SIZE
+ assert _header_hash(raw) == ReferenceData.KNOWN_BLOCK_HASH
+
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# BLOCK FILTERS (basic / neutrino; requires [blockchain] bip158 = true)
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_blockfilter_basic_json(bitcoind_rest_config):
+ """GET /rest/blockfilter/basic/.json returns a compact block filter."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"blockfilter/basic/{ReferenceData.KNOWN_BLOCK_HASH}.json"
+ )
+
+ assert isinstance(data, dict)
+ assert "filter" in data
+
+
+def test_blockfilterheaders_basic_json(bitcoind_rest_config):
+ """GET /rest/blockfilterheaders/basic/.json returns filter headers."""
+ data = get_json(
+ bitcoind_rest_config,
+ f"blockfilterheaders/basic/{ReferenceData.KNOWN_BLOCK_HASH}.json"
+ )
+
+ assert isinstance(data, (dict, list))
diff --git a/endpoints/test_bitcoind_rpc.py b/endpoints/test_bitcoind_rpc.py
index 38fe54b3..70f5e7c9 100644
--- a/endpoints/test_bitcoind_rpc.py
+++ b/endpoints/test_bitcoind_rpc.py
@@ -18,7 +18,13 @@
import warnings
from typing import Optional, Dict, Any
-from utils import ReferenceData, TestConfig
+from utils import (
+ ReferenceData,
+ TestConfig,
+ double_sha256,
+ reverse_hex,
+ validate_hex_hash,
+)
def send_rpc(
@@ -119,6 +125,48 @@ def parse_response(raw_data: str) -> dict:
return resp
+def raw_rpc(
+ config: dict,
+ method: str,
+ params: Optional[list] = None
+) -> Dict[str, Any]:
+ """
+ Send a JSON-RPC 2.0 request and return the full response without xfail.
+
+ Unlike send_rpc, this does not treat an error response as an expected
+ failure, so error paths (unknown txid, invalid input) can be asserted.
+ """
+ payload = {
+ "jsonrpc": "2.0",
+ "id": 0,
+ "method": method,
+ "params": params if params is not None else [],
+ }
+
+ if os.getenv("BITCOIND_DEBUG"):
+ print(">>>", json.dumps(payload, indent=2), flush=True)
+
+ try:
+ response = requests.post(
+ config["url"],
+ json=payload,
+ headers={"Content-Type": "application/json", "Connection": "close"},
+ auth=config.get("auth"),
+ timeout=config.get("timeout", TestConfig.DEFAULT_RPC_TIMEOUT)
+ )
+ response.raise_for_status()
+ data = response.json()
+ except requests.exceptions.RequestException as e:
+ raise RuntimeError(f"RPC connection error: {e}")
+ except ValueError:
+ raise RuntimeError("Invalid JSON response from RPC server")
+
+ if os.getenv("BITCOIND_DEBUG"):
+ print(f"<<< {method}:", json.dumps(data, indent=2), flush=True)
+
+ return data
+
+
# ═══════════════════════════════════════════════════════════════════════════════
# BLOCKCHAIN METHODS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -172,13 +220,15 @@ def test_getblockchaininfo(bitcoind_rpc_config):
assert "bits" in result
assert "blocks" in result
assert "chain" in result
- assert "chainwork" in result
+ # chainwork and size_on_disk are intentionally omitted by the implementation:
+ # chainwork needs a cumulative-work index (cf. getchainwork, not implemented)
+ # and size_on_disk needs store-size accounting. The remaining Core fields are
+ # returned.
assert "difficulty" in result
assert "headers" in result
assert "initialblockdownload" in result
assert "mediantime" in result
assert "pruned" in result
- assert "size_on_disk" in result
assert "target" in result
assert "time" in result
assert "verificationprogress" in result
@@ -376,6 +426,126 @@ def test_verifytxoutset(bitcoind_rpc_config):
assert "total_amount" in result
+# ═══════════════════════════════════════════════════════════════════════════════
+# RAW TRANSACTION METHODS
+# ═══════════════════════════════════════════════════════════════════════════════
+
+def test_getrawtransaction_raw(bitcoind_rpc_config):
+ """getrawtransaction verbosity=0 returns the serialized transaction hex."""
+ # Block 170 transaction (first payment, non-segwit) round-trips to its txid.
+ response = send_rpc(
+ bitcoind_rpc_config,
+ "getrawtransaction",
+ [ReferenceData.FIRST_TX_HASH, 0]
+ )
+
+ result = response["result"]
+ assert isinstance(result, str)
+ assert len(result) % 2 == 0
+ assert validate_hex_hash(result, len(result))
+ # For a non-witness transaction txid == reverse(double_sha256(serialized)).
+ assert reverse_hex(double_sha256(result)) == ReferenceData.FIRST_TX_HASH
+
+
+def test_getrawtransaction_verbose(bitcoind_rpc_config):
+ """getrawtransaction verbosity=1 returns a decoded tx with chain context."""
+ response = send_rpc(
+ bitcoind_rpc_config,
+ "getrawtransaction",
+ [ReferenceData.KNOWN_TX_HASH, 1]
+ )
+
+ result = response["result"]
+ assert isinstance(result, dict)
+ assert result["txid"] == ReferenceData.KNOWN_TX_HASH
+ assert isinstance(result["vin"], list) and len(result["vin"]) > 0
+ assert isinstance(result["vout"], list) and len(result["vout"]) > 0
+ # Context injected at the protocol layer (the serializer omits these).
+ assert result["blockhash"] == ReferenceData.KNOWN_BLOCK_HASH
+ assert result["in_active_chain"] is True
+ assert isinstance(result["confirmations"], int) and result["confirmations"] > 0
+
+
+def test_getrawtransaction_coinbase(bitcoind_rpc_config):
+ """getrawtransaction serves coinbase transactions (block 1 coinbase)."""
+ response = send_rpc(
+ bitcoind_rpc_config,
+ "getrawtransaction",
+ [ReferenceData.BLOCK1_COINBASE_TX_HASH, 1]
+ )
+
+ result = response["result"]
+ assert isinstance(result, dict)
+ assert result["txid"] == ReferenceData.BLOCK1_COINBASE_TX_HASH
+ assert isinstance(result["vin"], list) and len(result["vin"]) == 1
+ # Non-segwit transaction: the witness txid (hash) equals the txid.
+ if "hash" in result:
+ assert result["hash"] == result["txid"]
+
+
+def test_getrawtransaction_segwit(bitcoind_rpc_config):
+ """getrawtransaction handles segwit transactions (witness serialization)."""
+ response = send_rpc(
+ bitcoind_rpc_config,
+ "getrawtransaction",
+ [ReferenceData.SEGWIT_TX_HASH_P2WPKH, 1]
+ )
+
+ result = response["result"]
+ assert isinstance(result, dict)
+ assert result["txid"] == ReferenceData.SEGWIT_TX_HASH_P2WPKH
+ assert isinstance(result["vin"], list) and len(result["vin"]) > 0
+ assert isinstance(result["vout"], list) and len(result["vout"]) > 0
+ # Witness is serialized, so the witness txid (hash) differs from the txid.
+ assert result["hash"] != result["txid"]
+ # Segwit weight accounting: vsize == ceil(weight / 4).
+ # (Note: libbitcoin reports "size" as the stripped, non-witness size, whereas
+ # Core reports the total witnessed size, so size/vsize ordering is not
+ # asserted here.)
+ if "weight" in result and "vsize" in result:
+ assert result["vsize"] == (result["weight"] + 3) // 4
+
+
+def test_getrawtransaction_unknown(bitcoind_rpc_config):
+ """getrawtransaction returns a JSON-RPC error for an unknown txid."""
+ data = raw_rpc(bitcoind_rpc_config, "getrawtransaction", ["00" * 32, 1])
+ assert data.get("error") is not None
+ assert data.get("result") is None
+
+
+def test_sendrawtransaction_invalid_hex(bitcoind_rpc_config):
+ """sendrawtransaction rejects non-hexadecimal input with an error."""
+ data = raw_rpc(bitcoind_rpc_config, "sendrawtransaction", ["nothexZZ"])
+ assert data.get("error") is not None
+
+
+def test_sendrawtransaction_malformed(bitcoind_rpc_config):
+ """sendrawtransaction rejects hex that does not deserialize to a tx."""
+ data = raw_rpc(bitcoind_rpc_config, "sendrawtransaction", ["00"])
+ assert data.get("error") is not None
+
+
+@pytest.mark.skipif(
+ not os.getenv("BITCOIND_ALLOW_BROADCAST"),
+ reason="set BITCOIND_ALLOW_BROADCAST=1 to exercise the broadcast path "
+ "(re-announces an already-confirmed tx to connected peers)"
+)
+def test_sendrawtransaction_known(bitcoind_rpc_config):
+ """sendrawtransaction accepts a valid tx and echoes its txid.
+
+ Gated behind BITCOIND_ALLOW_BROADCAST: it announces an inv to peers. The
+ transaction is already mined, so acceptance is idempotent and benign.
+ """
+ raw = send_rpc(
+ bitcoind_rpc_config, "getrawtransaction",
+ [ReferenceData.FIRST_TX_HASH, 0]
+ )["result"]
+
+ data = raw_rpc(bitcoind_rpc_config, "sendrawtransaction", [raw])
+ assert data.get("error") is None
+ assert data["result"] == ReferenceData.FIRST_TX_HASH
+
+
# ═══════════════════════════════════════════════════════════════════════════════
# NETWORK METHODS
# ═══════════════════════════════════════════════════════════════════════════════
diff --git a/include/bitcoin/server/interfaces/bitcoind_rpc.hpp b/include/bitcoin/server/interfaces/bitcoind_rpc.hpp
index 1fede90b..7c83d608 100644
--- a/include/bitcoin/server/interfaces/bitcoind_rpc.hpp
+++ b/include/bitcoin/server/interfaces/bitcoind_rpc.hpp
@@ -38,7 +38,7 @@ struct bitcoind_rpc_methods
method<"getblockfilter", string_t, optional<"basic"_t>>{ "blockhash", "filtertype" },
method<"getblockhash", number_t>{ "height" },
method<"getblockheader", string_t, optional>{ "blockhash", "verbose" },
- method<"getblockstats", string_t, optional>{ "hash_or_height", "stats" },
+ method<"getblockstats", value_t, optional>{ "hash_or_height", "stats" },
method<"getchaintxstats", optional<-1.0>, optional<""_t>>{ "nblocks", "blockhash" },
method<"getchainwork">{},
method<"gettxout", string_t, number_t, optional>{ "txid", "n", "include_mempool" },
@@ -70,7 +70,11 @@ struct bitcoind_rpc_methods
////method<"disconnectnode", string_t, optional<-1_i32>>{ "address", "nodeid" },
////method<"getaddednodeinfo", optional, optional, optional<""_t>>{ "include_chain_info", "dns", "addnode" },
////method<"getconnectioncount">{},
- method<"getnetworkinfo">{}
+ method<"getnetworkinfo">{},
+
+ /// Rawtransactions methods (implemented).
+ method<"getrawtransaction", string_t, optional<0.0>, optional<""_t>>{ "txid", "verbosity", "blockhash" },
+ method<"sendrawtransaction", string_t, optional<0.0>>{ "hexstring", "maxfeerate" }
////method<"getpeerinfo">{},
////method<"listbanned">{},
////method<"ping">{},
@@ -188,6 +192,8 @@ struct bitcoind_rpc_methods
////using get_added_node_info = at<31>;
////using get_connection_count = at<32>;
using get_network_info = at<17>;
+ using get_raw_transaction = at<18>;
+ using send_raw_transaction = at<19>;
////using get_peer_info = at<34>;
////using list_banned = at<35>;
////using ping = at<36>;
diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp
index e5511113..57e36102 100644
--- a/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp
+++ b/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp
@@ -57,7 +57,31 @@ class BCS_API protocol_bitcoind_rest
/// REST interface handlers.
bool handle_get_block(const code& ec, rest_interface::block,
- uint8_t media, system::hash_cptr hash) NOEXCEPT;
+ uint8_t media, const system::hash_cptr& hash) NOEXCEPT;
+ bool handle_get_block_hash(const code& ec, rest_interface::block_hash,
+ uint8_t media, uint32_t height) NOEXCEPT;
+ bool handle_get_block_txs(const code& ec, rest_interface::block_txs,
+ uint8_t media, const system::hash_cptr& hash) NOEXCEPT;
+ bool handle_get_block_headers(const code& ec, rest_interface::block_headers,
+ uint8_t media, const system::hash_cptr& hash, uint32_t count) NOEXCEPT;
+ bool handle_get_block_part(const code& ec, rest_interface::block_part,
+ uint8_t media, const system::hash_cptr& hash, uint32_t offset,
+ uint32_t size) NOEXCEPT;
+ bool handle_get_block_spent_tx_outputs(const code& ec,
+ rest_interface::block_spent_tx_outputs, uint8_t media,
+ const system::hash_cptr& hash) NOEXCEPT;
+ bool handle_get_block_filter(const code& ec, rest_interface::block_filter,
+ uint8_t media, const system::hash_cptr& hash, uint8_t type) NOEXCEPT;
+ bool handle_get_block_filter_headers(const code& ec,
+ rest_interface::block_filter_headers, uint8_t media,
+ const system::hash_cptr& hash, uint8_t type) NOEXCEPT;
+ bool handle_get_chain_information(const code& ec,
+ rest_interface::chain_information) NOEXCEPT;
+
+ /// REST raw-http response senders (not json-rpc enveloped).
+ void send_data(system::data_chunk&& bytes) NOEXCEPT;
+ void send_hex(std::string&& text) NOEXCEPT;
+ void send_dom(boost::json::value&& model, size_t size_hint) NOEXCEPT;
private:
template
diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp
index 9462ab36..a374b760 100644
--- a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp
+++ b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
namespace libbitcoin {
namespace server {
@@ -79,7 +80,7 @@ class BCS_API protocol_bitcoind_rpc
bool handle_get_block_header(const code& ec,
rpc_interface::get_block_header, const std::string&, bool) NOEXCEPT;
bool handle_get_block_stats(const code& ec,
- rpc_interface::get_block_stats, const std::string&,
+ rpc_interface::get_block_stats, const network::rpc::value_t&,
const network::rpc::array_t&) NOEXCEPT;
bool handle_get_chain_tx_stats(const code& ec,
rpc_interface::get_chain_tx_stats, double,
@@ -103,6 +104,24 @@ class BCS_API protocol_bitcoind_rpc
rpc_interface::verify_tx_out_set, const std::string&) NOEXCEPT;
bool handle_get_network_info(const code& ec,
rpc_interface::get_network_info) NOEXCEPT;
+ bool handle_get_raw_transaction(const code& ec,
+ rpc_interface::get_raw_transaction, const std::string& txid,
+ double verbose, const std::string& blockhash) NOEXCEPT;
+ bool handle_send_raw_transaction(const code& ec,
+ rpc_interface::send_raw_transaction, const std::string& hexstring,
+ double maxfeerate) NOEXCEPT;
+
+ /// Json context helpers (shared with rest, defined in *_json.cpp).
+ static uint32_t median_time_past(const node::query& query,
+ size_t height) NOEXCEPT;
+ static void inject_block_context(boost::json::object& out,
+ const node::query& query, const database::header_link& link,
+ const system::chain::header& header) NOEXCEPT;
+ static void inject_tx_context(boost::json::object& out,
+ const node::query& query, const database::tx_link& link) NOEXCEPT;
+ static boost::json::object header_to_bitcoind(
+ const system::chain::header& header) NOEXCEPT;
+ static std::string chain_name(const node::query& query) NOEXCEPT;
/// Senders.
void send_error(const code& ec) NOEXCEPT;
diff --git a/src/parsers/bitcoind_target.cpp b/src/parsers/bitcoind_target.cpp
index 35d1e462..ca5f17b6 100644
--- a/src/parsers/bitcoind_target.cpp
+++ b/src/parsers/bitcoind_target.cpp
@@ -18,6 +18,8 @@
*/
#include
+#include
+#include
#include
#include
#include
@@ -28,13 +30,257 @@ namespace server {
using namespace system;
using namespace network::rpc;
+using namespace network::http;
BC_PUSH_WARNING(NO_ARRAY_INDEXING)
BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
-code bitcoind_target(request_t& , const std::string_view& ) NOEXCEPT
+static hash_cptr to_hash(const std::string_view& token) NOEXCEPT
{
- return {};
+ hash_digest out{};
+ return decode_hash(out, token) ?
+ to_shared(std::move(out)) : hash_cptr{};
+}
+
+// Map a Bitcoin Core REST file extension to a media value.
+static bool to_media(uint8_t& out, const std::string_view& extension) NOEXCEPT
+{
+ if (extension == "bin")
+ {
+ out = to_value(media_type::application_octet_stream);
+ return true;
+ }
+ if (extension == "hex")
+ {
+ out = to_value(media_type::text_plain);
+ return true;
+ }
+ if (extension == "json")
+ {
+ out = to_value(media_type::application_json);
+ return true;
+ }
+
+ return false;
+}
+
+template
+static bool to_number(Number& out, const std::string_view& token) NOEXCEPT
+{
+ if (token.empty() || (token.size() > one && token.front() == '0'))
+ return false;
+
+ const auto end = std::next(token.data(), token.size());
+ const auto result = std::from_chars(token.data(), end, out);
+ return result.ec == std::errc{} && result.ptr == end;
+}
+
+// Split a "." leaf into its name and a media value.
+static bool split_leaf(std::string& name, uint8_t& media,
+ const std::string_view& leaf) NOEXCEPT
+{
+ const auto parts = split(leaf, ".", false, true);
+ if (parts.size() != two)
+ return false;
+
+ name = parts.front();
+ return to_media(media, parts.back());
+}
+
+// Parse a Bitcoin Core REST path into a json-rpc request model.
+// github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md
+// Supports: block, block/notxdetails, blockhashbyheight, headers, blockpart,
+// chaininfo (remaining endpoints return invalid_target until implemented).
+code bitcoind_target(request_t& out, const std::string_view& path) NOEXCEPT
+{
+ const auto clean = split(path, "?", false, false).front();
+ if (clean.empty())
+ return error::empty_path;
+
+ // Avoid conflict with node type.
+ using object_t = network::rpc::object_t;
+
+ // Initialize json-rpc.v2 named params message.
+ out = request_t
+ {
+ .jsonrpc = version::v2,
+ .id = null_t{},
+ .method = {},
+ .params = object_t{}
+ };
+
+ auto& method = out.method;
+ auto& params = std::get(out.params.value());
+ const auto segments = split(clean, "/", false, true);
+ BC_ASSERT(!segments.empty());
+
+ size_t segment{};
+
+ // Accept an optional "rest" prefix (Core mounts endpoints under /rest/).
+ if (segments[segment] == "rest")
+ ++segment;
+
+ if (segment == segments.size())
+ return error::missing_target;
+
+ const auto target = segments[segment++];
+
+ // /rest/chaininfo.json
+ if (target == "chaininfo" || target == "chaininfo.json")
+ {
+ method = "chain_information";
+ return error::success;
+ }
+
+ // /rest/block/., /rest/block/notxdetails/. and
+ // /rest/block/spent/. (the latter is a libbitcoin extension).
+ if (target == "block")
+ {
+ if (segment == segments.size())
+ return error::missing_hash;
+
+ std::string rest_method = "block";
+ if (segments[segment] == "notxdetails")
+ rest_method = "block_txs";
+ else if (segments[segment] == "spent")
+ rest_method = "block_spent_tx_outputs";
+
+ if (rest_method != "block" && ++segment == segments.size())
+ return error::missing_hash;
+
+ std::string name{};
+ uint8_t media{};
+ if (!split_leaf(name, media, segments[segment++]))
+ return error::invalid_target;
+
+ const auto hash = to_hash(name);
+ if (!hash)
+ return error::invalid_hash;
+
+ method = rest_method;
+ params["media"] = media;
+ params["hash"] = hash;
+ return error::success;
+ }
+
+ // /rest/blockhashbyheight/.
+ if (target == "blockhashbyheight")
+ {
+ if (segment == segments.size())
+ return error::missing_target;
+
+ std::string name{};
+ uint8_t media{};
+ if (!split_leaf(name, media, segments[segment++]))
+ return error::invalid_target;
+
+ uint32_t height{};
+ if (!to_number(height, name))
+ return error::invalid_number;
+
+ method = "block_hash";
+ params["media"] = media;
+ params["height"] = height;
+ return error::success;
+ }
+
+ // /rest/headers//.
+ if (target == "headers")
+ {
+ if (segment == segments.size())
+ return error::missing_target;
+
+ uint32_t count{};
+ if (!to_number(count, segments[segment++]))
+ return error::invalid_number;
+
+ if (segment == segments.size())
+ return error::missing_hash;
+
+ std::string name{};
+ uint8_t media{};
+ if (!split_leaf(name, media, segments[segment++]))
+ return error::invalid_target;
+
+ const auto hash = to_hash(name);
+ if (!hash)
+ return error::invalid_hash;
+
+ method = "block_headers";
+ params["media"] = media;
+ params["hash"] = hash;
+ params["count"] = count;
+ return error::success;
+ }
+
+ // /rest/blockfilter//. and blockfilterheaders likewise.
+ if (target == "blockfilter" || target == "blockfilterheaders")
+ {
+ if (segment == segments.size())
+ return error::missing_target;
+
+ // libbitcoin supports only the "basic" (neutrino) filter type.
+ if (segments[segment++] != "basic")
+ return error::invalid_target;
+
+ if (segment == segments.size())
+ return error::missing_hash;
+
+ std::string name{};
+ uint8_t media{};
+ if (!split_leaf(name, media, segments[segment++]))
+ return error::invalid_target;
+
+ const auto hash = to_hash(name);
+ if (!hash)
+ return error::invalid_hash;
+
+ method = target == "blockfilter" ? "block_filter" :
+ "block_filter_headers";
+ params["media"] = media;
+ params["hash"] = hash;
+ params["type"] = uint8_t{ 0 };
+ return error::success;
+ }
+
+ // /rest/blockpart///. (libbitcoin extension)
+ if (target == "blockpart")
+ {
+ if (segment == segments.size())
+ return error::missing_hash;
+
+ const auto hash = to_hash(segments[segment++]);
+ if (!hash)
+ return error::invalid_hash;
+
+ if (segment == segments.size())
+ return error::missing_target;
+
+ uint32_t offset{};
+ if (!to_number(offset, segments[segment++]))
+ return error::invalid_number;
+
+ if (segment == segments.size())
+ return error::missing_target;
+
+ std::string name{};
+ uint8_t media{};
+ if (!split_leaf(name, media, segments[segment++]))
+ return error::invalid_target;
+
+ uint32_t size{};
+ if (!to_number(size, name))
+ return error::invalid_number;
+
+ method = "block_part";
+ params["media"] = media;
+ params["hash"] = hash;
+ params["offset"] = offset;
+ params["size"] = size;
+ return error::success;
+ }
+
+ return error::invalid_target;
}
BC_POP_WARNING()
diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp
index 281a2a23..0d831037 100644
--- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp
+++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp
@@ -18,9 +18,13 @@
*/
#include
+#include
+#include
+#include
#include
#include
#include
+#include
namespace libbitcoin {
namespace server {
@@ -50,6 +54,14 @@ void protocol_bitcoind_rest::start() NOEXCEPT
return;
SUBSCRIBE_BITCOIND(handle_get_block, _1, _2, _3, _4);
+ SUBSCRIBE_BITCOIND(handle_get_block_hash, _1, _2, _3, _4);
+ SUBSCRIBE_BITCOIND(handle_get_block_txs, _1, _2, _3, _4);
+ SUBSCRIBE_BITCOIND(handle_get_block_headers, _1, _2, _3, _4, _5);
+ SUBSCRIBE_BITCOIND(handle_get_block_part, _1, _2, _3, _4, _5, _6);
+ SUBSCRIBE_BITCOIND(handle_get_block_spent_tx_outputs, _1, _2, _3, _4);
+ SUBSCRIBE_BITCOIND(handle_get_block_filter, _1, _2, _3, _4, _5);
+ SUBSCRIBE_BITCOIND(handle_get_block_filter_headers, _1, _2, _3, _4, _5);
+ SUBSCRIBE_BITCOIND(handle_get_chain_information, _1, _2);
protocol_bitcoind_rpc::start();
}
@@ -86,50 +98,527 @@ void protocol_bitcoind_rest::handle_receive_get(const code& ec,
return;
}
- // The post is saved off during asynchonous handling and used in send_json
+ // The get is saved off during asynchonous handling and used in send_json
// to formulate response headers, isolating handlers from http semantics.
set_request(get);
- ////if (const auto code = rest_dispatcher_.notify({}))
- //// stop(code);
- protocol_bitcoind_rpc::handle_receive_get(ec, get);
+ // Parse the REST url into a json-rpc model and dispatch to a handler.
+ request_t model{};
+ if (bitcoind_target(model, get->target()))
+ {
+ send_not_found();
+ return;
+ }
+
+ if (rest_dispatcher_.notify(model))
+ send_not_found();
}
// Handlers.
// ----------------------------------------------------------------------------
-////constexpr auto data = to_value(media_type::application_octet_stream);
-////constexpr auto json = to_value(media_type::application_json);
-////constexpr auto text = to_value(media_type::text_plain);
+constexpr auto data = to_value(media_type::application_octet_stream);
+constexpr auto json = to_value(media_type::application_json);
+constexpr auto text = to_value(media_type::text_plain);
+
+template
+data_chunk to_bin(const Object& object, size_t size, Args&&... args) NOEXCEPT
+{
+ data_chunk out(size);
+ stream::out::fast sink{ out };
+ write::bytes::fast writer{ sink };
+ object.to_data(writer, std::forward(args)...);
+ return out;
+}
+
+template
+std::string to_hex(const Object& object, size_t size, Args&&... args) NOEXCEPT
+{
+ std::string out(two * size, '\0');
+ stream::out::fast sink{ out };
+ write::base16::fast writer{ sink };
+ object.to_data(writer, std::forward(args)...);
+ return out;
+}
bool protocol_bitcoind_rest::handle_get_block(const code& ec,
- rest_interface::block, uint8_t , system::hash_cptr ) NOEXCEPT
+ rest_interface::block, uint8_t media, const system::hash_cptr& hash) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ if (!hash)
+ {
+ send_not_found();
+ return true;
+ }
+
+ constexpr auto witness = true;
+ const auto& query = archive();
+ const auto block = query.get_block(query.to_header(*hash), witness);
+ if (is_null(block))
+ {
+ send_not_found();
+ return true;
+ }
+
+ const auto size = block->serialized_size(witness);
+ switch (media)
+ {
+ case data:
+ send_data(to_bin(*block, size, witness));
+ return true;
+ case text:
+ send_hex(to_hex(*block, size, witness));
+ return true;
+ case json:
+ send_dom(value_from(bitcoind_verbose(*block)), two * size);
+ return true;
+ }
+
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_block_hash(const code& ec,
+ rest_interface::block_hash, uint8_t media, uint32_t height) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ const auto& query = archive();
+ const auto link = query.to_confirmed(height);
+ if (link.is_terminal())
+ {
+ send_not_found();
+ return true;
+ }
+
+ const auto hash = query.get_header_key(link);
+ switch (media)
+ {
+ case data:
+ send_data(to_chunk(hash));
+ return true;
+ case text:
+ send_hex(encode_base16(hash));
+ return true;
+ case json:
+ send_dom(boost::json::object{ { "blockhash", encode_hash(hash) } },
+ two * hash_size);
+ return true;
+ }
+
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_block_txs(const code& ec,
+ rest_interface::block_txs, uint8_t media, const system::hash_cptr& hash) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ if (!hash)
+ {
+ send_not_found();
+ return true;
+ }
+
+ constexpr auto witness = true;
+ const auto& query = archive();
+ const auto block = query.get_block(query.to_header(*hash), witness);
+ if (is_null(block))
+ {
+ send_not_found();
+ return true;
+ }
+
+ // notxdetails: raw block for bin/hex, txid list (bitcoind_hashed) for json.
+ const auto size = block->serialized_size(witness);
+ switch (media)
+ {
+ case data:
+ send_data(to_bin(*block, size, witness));
+ return true;
+ case text:
+ send_hex(to_hex(*block, size, witness));
+ return true;
+ case json:
+ send_dom(value_from(bitcoind_hashed(*block)), two * size);
+ return true;
+ }
+
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_block_headers(const code& ec,
+ rest_interface::block_headers, uint8_t media, const system::hash_cptr& hash,
+ uint32_t count) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ if (!hash || is_zero(count))
+ {
+ send_not_found();
+ return true;
+ }
+
+ const auto& query = archive();
+ const auto link = query.to_header(*hash);
+ size_t height{};
+ if (link.is_terminal() || !query.get_height(height, link))
+ {
+ send_not_found();
+ return true;
+ }
+
+ // Core caps the header count at 2000.
+ constexpr size_t maximum = 2000;
+ constexpr auto header_size = chain::header::serialized_size();
+ const auto top = query.get_top_confirmed();
+ const auto limit = std::min(static_cast(count), maximum);
+
+ std::vector headers{};
+ headers.reserve(limit);
+ for (size_t index = 0; index < limit && height + index <= top; ++index)
+ {
+ const auto header = query.get_header(query.to_confirmed(height + index));
+ if (!header)
+ break;
+
+ headers.push_back(header);
+ }
+
+ if (headers.empty())
+ {
+ send_not_found();
+ return true;
+ }
+
+ switch (media)
+ {
+ case data:
+ {
+ data_chunk out{};
+ out.reserve(headers.size() * header_size);
+ for (const auto& header: headers)
+ {
+ const auto bin = to_bin(*header, header_size);
+ out.insert(out.end(), bin.begin(), bin.end());
+ }
+
+ send_data(std::move(out));
+ return true;
+ }
+ case text:
+ {
+ std::string out{};
+ out.reserve(headers.size() * two * header_size);
+ for (const auto& header: headers)
+ out += to_hex(*header, header_size);
+
+ send_hex(std::move(out));
+ return true;
+ }
+ case json:
+ {
+ boost::json::array out{};
+ out.reserve(headers.size());
+ for (const auto& header: headers)
+ out.push_back(header_to_bitcoind(*header));
+
+ send_dom(std::move(out), headers.size() * two * header_size);
+ return true;
+ }
+ }
+
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_block_part(const code& ec,
+ rest_interface::block_part, uint8_t media, const system::hash_cptr& hash,
+ uint32_t offset, uint32_t size) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ if (!hash)
+ {
+ send_not_found();
+ return true;
+ }
+
+ constexpr auto witness = true;
+ const auto& query = archive();
+ const auto block = query.get_block(query.to_header(*hash), witness);
+ if (is_null(block))
+ {
+ send_not_found();
+ return true;
+ }
+
+ const auto full = to_bin(*block, block->serialized_size(witness), witness);
+ if (offset >= full.size())
+ {
+ send_not_found();
+ return true;
+ }
+
+ const auto end = std::min(static_cast(offset) + size, full.size());
+ data_chunk part(full.begin() + offset, full.begin() + end);
+ switch (media)
+ {
+ case data:
+ send_data(std::move(part));
+ return true;
+ case text:
+ send_hex(encode_base16(part));
+ return true;
+ }
+
+ // block_part is bin|hex only (json not supported).
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_block_spent_tx_outputs(const code& ec,
+ rest_interface::block_spent_tx_outputs, uint8_t media,
+ const system::hash_cptr& hash) NOEXCEPT
{
if (stopped(ec))
return false;
- ////const auto& query = archive();
- ////if (const auto block = query.get_block(query.to_header(*hash), true))
- ////{
- //// const auto size = block->serialized_size(true);
- //// switch (media)
- //// {
- //// case data:
- //// send_chunk(to_bin(*block, size, true));
- //// return true;
- //// case text:
- //// send_text(to_hex(*block, size, true));
- //// return true;
- //// case json:
- //// send_json(value_from(block), two * size);
- //// return true;
- //// }
- ////}
+ if (!hash)
+ {
+ send_not_found();
+ return true;
+ }
+
+ constexpr auto witness = true;
+ const auto& query = archive();
+ const auto block = query.get_block(query.to_header(*hash), witness);
+ if (is_null(block))
+ {
+ send_not_found();
+ return true;
+ }
+
+ // Resolve every prevout spent by the block's non-coinbase transactions.
+ chain::output_cptrs spent{};
+ const auto& txs = *block->transactions_ptr();
+ for (size_t tx = 1; tx < txs.size(); ++tx)
+ for (const auto& in: *txs.at(tx)->inputs_ptr())
+ if (const auto out = query.get_output(query.to_output(in->point())))
+ spent.push_back(out);
+
+ size_t size{};
+ for (const auto& output: spent)
+ size += output->serialized_size();
+
+ switch (media)
+ {
+ case data:
+ {
+ data_chunk out(size);
+ stream::out::fast sink{ out };
+ write::bytes::fast writer{ sink };
+ for (const auto& output: spent)
+ output->to_data(writer);
+
+ send_data(std::move(out));
+ return true;
+ }
+ case text:
+ {
+ std::string out(two * size, '\0');
+ stream::out::fast sink{ out };
+ write::base16::fast writer{ sink };
+ for (const auto& output: spent)
+ output->to_data(writer);
+
+ send_hex(std::move(out));
+ return true;
+ }
+ case json:
+ send_dom(value_from(bitcoind(spent)), two * size);
+ return true;
+ }
send_not_found();
return true;
}
+bool protocol_bitcoind_rest::handle_get_block_filter(const code& ec,
+ rest_interface::block_filter, uint8_t media, const system::hash_cptr& hash,
+ uint8_t) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ const auto& query = archive();
+ if (!hash || !query.filter_enabled())
+ {
+ send_not_found();
+ return true;
+ }
+
+ // libbitcoin stores only the neutrino (basic) filter; type is ignored.
+ data_chunk filter{};
+ if (!query.get_filter_body(filter, query.to_header(*hash)))
+ {
+ send_not_found();
+ return true;
+ }
+
+ switch (media)
+ {
+ case data:
+ send_data(std::move(filter));
+ return true;
+ case text:
+ send_hex(encode_base16(filter));
+ return true;
+ case json:
+ send_dom(boost::json::object
+ {
+ { "filter", encode_base16(filter) }
+ }, two * filter.size());
+ return true;
+ }
+
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_block_filter_headers(const code& ec,
+ rest_interface::block_filter_headers, uint8_t media, const system::hash_cptr& hash,
+ uint8_t) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ const auto& query = archive();
+ if (!hash || !query.filter_enabled())
+ {
+ send_not_found();
+ return true;
+ }
+
+ hash_digest filter_head{};
+ if (!query.get_filter_head(filter_head, query.to_header(*hash)))
+ {
+ send_not_found();
+ return true;
+ }
+
+ switch (media)
+ {
+ case data:
+ send_data(to_chunk(filter_head));
+ return true;
+ case text:
+ send_hex(encode_base16(filter_head));
+ return true;
+ case json:
+ send_dom(boost::json::object
+ {
+ { "filter_header", encode_hash(filter_head) }
+ }, two * hash_size);
+ return true;
+ }
+
+ send_not_found();
+ return true;
+}
+
+bool protocol_bitcoind_rest::handle_get_chain_information(const code& ec,
+ rest_interface::chain_information) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ const auto& query = archive();
+ const auto blocks = query.get_top_confirmed();
+ const auto link = query.to_confirmed(blocks);
+ const auto header = query.get_header(link);
+ if (is_null(header))
+ {
+ send_not_found();
+ return true;
+ }
+
+ send_dom(boost::json::object
+ {
+ { "chain", chain_name(query) },
+ { "blocks", static_cast(blocks) },
+ { "headers", static_cast(query.get_top_candidate()) },
+ { "bestblockhash", encode_hash(query.get_header_key(link)) },
+ { "bits", encode_base16(to_big_endian(header->bits())) },
+ { "difficulty", header->difficulty() },
+ { "time", header->timestamp() },
+ { "mediantime", median_time_past(query, blocks) },
+ { "pruned", false }
+ }, 256);
+ return true;
+}
+
+// Raw-http response senders (mirror protocol_html, not json-rpc enveloped).
+// ----------------------------------------------------------------------------
+
+void protocol_bitcoind_rest::send_data(data_chunk&& bytes) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+ const auto request = reset_request();
+ network::http::response message{ network::http::status::ok,
+ request->version() };
+ add_common_headers(message, *request);
+ add_access_control_headers(message, *request);
+ message.set(network::http::field::content_type, network::http::
+ from_media_type(media_type::application_octet_stream));
+ message.body() = std::move(bytes);
+ message.prepare_payload();
+ SEND(std::move(message), handle_complete, _1, error::success);
+}
+
+void protocol_bitcoind_rest::send_hex(std::string&& text) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+ const auto request = reset_request();
+ network::http::response message{ network::http::status::ok,
+ request->version() };
+ add_common_headers(message, *request);
+ add_access_control_headers(message, *request);
+ message.set(network::http::field::content_type, network::http::
+ from_media_type(media_type::text_plain));
+ message.body() = std::move(text);
+ message.prepare_payload();
+ SEND(std::move(message), handle_complete, _1, error::success);
+}
+
+void protocol_bitcoind_rest::send_dom(boost::json::value&& model,
+ size_t size_hint) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+ const auto request = reset_request();
+ network::http::response message{ network::http::status::ok,
+ request->version() };
+ add_common_headers(message, *request);
+ add_access_control_headers(message, *request);
+ message.set(network::http::field::content_type, network::http::
+ from_media_type(media_type::application_json));
+ message.body() = network::http::json_value
+ {
+ .model = std::move(model),
+ .size_hint = size_hint
+ };
+ message.prepare_payload();
+ SEND(std::move(message), handle_complete, _1, error::success);
+}
+
// private
// ----------------------------------------------------------------------------
diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp
index 2961185f..821fa125 100644
--- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp
+++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp
@@ -20,6 +20,8 @@
#include
#include
+#include
+#include
namespace libbitcoin {
namespace server {
@@ -66,6 +68,8 @@ void protocol_bitcoind_rpc::start() NOEXCEPT
SUBSCRIBE_BITCOIND(handle_verify_tx_out_set, _1, _2, _3);
SUBSCRIBE_BITCOIND(handle_get_network_info, _1, _2);
+ SUBSCRIBE_BITCOIND(handle_get_raw_transaction, _1, _2, _3, _4, _5);
+ SUBSCRIBE_BITCOIND(handle_send_raw_transaction, _1, _2, _3, _4);
network::protocol_http::start();
}
@@ -207,15 +211,23 @@ bool protocol_bitcoind_rpc::handle_get_block(const code& ec,
return true;
}
- if (verbosity == 1.0)
+ if (verbosity == 1.0 || verbosity == 2.0)
{
- send_error(error::not_implemented);
- return true;
- }
+ const auto block = query.get_block(link, witness);
+ if (is_null(block))
+ {
+ send_error(error::not_found, blockhash, blockhash.size());
+ return true;
+ }
- if (verbosity == 2.0)
- {
- send_error(error::not_implemented);
+ // verbosity 1 lists txids; verbosity 2 embeds full tx objects.
+ auto model = (verbosity == 1.0) ?
+ value_from(bitcoind_hashed(*block)) :
+ value_from(bitcoind_verbose(*block));
+
+ inject_block_context(model.as_object(), query, link, block->header());
+ send_result(rpc::value_t(std::move(model)),
+ two * block->serialized_size(witness));
return true;
}
@@ -226,46 +238,158 @@ bool protocol_bitcoind_rpc::handle_get_block(const code& ec,
bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec,
rpc_interface::get_block_chain_info) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ const auto& query = archive();
+ const auto blocks = query.get_top_confirmed();
+ const auto headers = query.get_top_candidate();
+ const auto link = query.to_confirmed(blocks);
+ const auto header = query.get_header(link);
+ if (is_null(header))
+ {
+ send_error(database::error::integrity);
+ return true;
+ }
+
+ // verificationprogress is approximated as confirmed/candidate height, the
+ // best available estimate of the chain tip during sync (1.0 once current).
+ const auto progress = is_zero(headers) ? 1.0 :
+ std::min(1.0, static_cast(blocks) /
+ static_cast(headers));
+
+ send_result(rpc::object_t
+ {
+ { "chain", chain_name(query) },
+ { "blocks", static_cast(blocks) },
+ { "headers", static_cast(headers) },
+ { "bestblockhash", encode_hash(query.get_header_key(link)) },
+ { "bits", encode_base16(to_big_endian(header->bits())) },
+ { "target", encode_hash(from_uintx(
+ chain::compact::expand(header->bits()))) },
+ { "difficulty", header->difficulty() },
+ { "time", header->timestamp() },
+ { "mediantime", median_time_past(query, blocks) },
+ { "verificationprogress", progress },
+ { "initialblockdownload", blocks < headers },
+ { "pruned", false },
+ { "warnings", std::string{} }
+ }, 512);
return true;
}
bool protocol_bitcoind_rpc::handle_get_block_count(const code& ec,
rpc_interface::get_block_count) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ send_result(rpc::value_t(static_cast(
+ archive().get_top_confirmed())), 20);
return true;
}
bool protocol_bitcoind_rpc::handle_get_block_filter(const code& ec,
- rpc_interface::get_block_filter, const std::string&,
+ rpc_interface::get_block_filter, const std::string& blockhash,
const std::string&) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ hash_digest hash{};
+ if (!decode_hash(hash, blockhash))
+ {
+ send_error(error::not_found, blockhash, blockhash.size());
+ return true;
+ }
+
+ const auto& query = archive();
+ if (!query.filter_enabled())
+ {
+ send_error(error::not_implemented);
+ return true;
+ }
+
+ const auto link = query.to_header(hash);
+ data_chunk filter{};
+ hash_digest filter_header{};
+ if (!query.get_filter_body(filter, link) ||
+ !query.get_filter_head(filter_header, link))
+ {
+ send_error(error::not_found, blockhash, blockhash.size());
+ return true;
+ }
+
+ send_result(rpc::object_t
+ {
+ { "filter", encode_base16(filter) },
+ { "header", encode_hash(filter_header) }
+ }, two * filter.size());
return true;
}
bool protocol_bitcoind_rpc::handle_get_block_hash(const code& ec,
- rpc_interface::get_block_hash, network::rpc::number_t) NOEXCEPT
+ rpc_interface::get_block_hash, double height) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ if (height < 0.0)
+ {
+ send_error(error::invalid_argument);
+ return true;
+ }
+
+ const auto& query = archive();
+ const auto link = query.to_confirmed(static_cast(height));
+ if (link.is_terminal())
+ {
+ send_error(error::not_found);
+ return true;
+ }
+
+ send_result(encode_hash(query.get_header_key(link)), two * hash_size);
return true;
}
bool protocol_bitcoind_rpc::handle_get_block_header(const code& ec,
- rpc_interface::get_block_header, const std::string&, bool) NOEXCEPT
+ rpc_interface::get_block_header, const std::string& blockhash,
+ bool verbose) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ hash_digest hash{};
+ if (!decode_hash(hash, blockhash))
+ {
+ send_error(error::not_found, blockhash, blockhash.size());
+ return true;
+ }
+
+ const auto& query = archive();
+ const auto link = query.to_header(hash);
+ const auto header = query.get_header(link);
+ if (is_null(header))
+ {
+ send_error(error::not_found, blockhash, blockhash.size());
+ return true;
+ }
+
+ if (!verbose)
+ {
+ send_text(to_hex(*header, chain::header::serialized_size()));
+ return true;
+ }
+
+ auto out = header_to_bitcoind(*header);
+ out["nTx"] = static_cast(query.get_tx_count(link));
+ inject_block_context(out, query, link, *header);
+ send_result(rpc::value_t(boost::json::value(std::move(out))), 512);
return true;
}
bool protocol_bitcoind_rpc::handle_get_block_stats(const code& ec,
- rpc_interface::get_block_stats, const std::string&,
+ rpc_interface::get_block_stats, const network::rpc::value_t&,
const network::rpc::array_t&) NOEXCEPT
{
if (stopped(ec)) return false;
@@ -290,10 +414,51 @@ bool protocol_bitcoind_rpc::handle_get_chain_work(const code& ec,
}
bool protocol_bitcoind_rpc::handle_get_tx_out(const code& ec,
- rpc_interface::get_tx_out, const std::string&, double, bool) NOEXCEPT
+ rpc_interface::get_tx_out, const std::string& txid, double n,
+ bool) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ hash_digest hash{};
+ if (!decode_hash(hash, txid) || n < 0.0)
+ {
+ send_error(error::invalid_argument);
+ return true;
+ }
+
+ const auto& query = archive();
+ const auto index = static_cast(n);
+ const auto output_fk = query.to_output(hash, index);
+
+ // Core returns json null for a missing or spent output (mempool ignored).
+ if (output_fk.is_terminal() || query.is_spent(output_fk))
+ {
+ send_result(rpc::value_t{}, 4);
+ return true;
+ }
+
+ const auto output = query.get_output(output_fk);
+ if (is_null(output))
+ {
+ send_result(rpc::value_t{}, 4);
+ return true;
+ }
+
+ const auto tx_fk = query.to_tx(hash);
+ size_t tx_height{};
+ const auto have_height = query.get_tx_height(tx_height, tx_fk);
+ const auto top = query.get_top_confirmed();
+
+ send_result(rpc::object_t
+ {
+ { "bestblock", encode_hash(query.get_top_confirmed_hash()) },
+ { "confirmations", have_height ?
+ static_cast(top - tx_height + 1u) : int64_t{ 0 } },
+ { "value", static_cast(output->value()) / 100000000.0 },
+ { "scriptPubKey", value_from(bitcoind(output->script())) },
+ { "coinbase", query.is_coinbase(tx_fk) }
+ }, 256);
return true;
}
@@ -349,8 +514,121 @@ bool protocol_bitcoind_rpc::handle_verify_tx_out_set(const code& ec,
bool protocol_bitcoind_rpc::handle_get_network_info(const code& ec,
rpc_interface::get_network_info) NOEXCEPT
{
- if (stopped(ec)) return false;
- send_error(error::not_implemented);
+ if (stopped(ec))
+ return false;
+
+ // libbitcoin-server is a node, not a wallet/peer-introspection service;
+ // peer-dependent fields (connections, addresses) are reported as empty.
+ // TODO: surface live connection count and relay fee from node settings.
+ send_result(rpc::object_t
+ {
+ { "version", 0 },
+ { "subversion", std::string{ "/libbitcoin:server/" } },
+ { "protocolversion", 70016 },
+ { "localrelay", true },
+ { "timeoffset", 0 },
+ { "connections", 0 },
+ { "networkactive", true },
+ { "networks", rpc::array_t{} },
+ { "relayfee", 0.00001 },
+ { "incrementalfee", 0.00001 },
+ { "localaddresses", rpc::array_t{} },
+ { "warnings", std::string{} }
+ }, 256);
+ return true;
+}
+
+// Rawtransactions methods.
+// ----------------------------------------------------------------------------
+
+bool protocol_bitcoind_rpc::handle_get_raw_transaction(const code& ec,
+ rpc_interface::get_raw_transaction, const std::string& txid,
+ double verbose, const std::string&) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ // The blockhash hint is unused: libbitcoin archives all tx (global index).
+ hash_digest hash{};
+ if (!decode_hash(hash, txid))
+ {
+ send_error(error::not_found, txid, txid.size());
+ return true;
+ }
+
+ constexpr auto witness = true;
+ auto& query = archive();
+ const auto link = query.to_tx(hash);
+ const auto tx = query.get_transaction(link, witness);
+ if (is_null(tx))
+ {
+ send_error(error::not_found, txid, txid.size());
+ return true;
+ }
+
+ if (verbose == 0.0)
+ {
+ send_text(to_hex(*tx, tx->serialized_size(witness), witness));
+ return true;
+ }
+
+ // bitcoind() (not bitcoind_verbose) yields Core's tx fields: txid/hash/
+ // size/vsize/weight/vin/vout/hex (bitcoind_verbose on a standalone tx
+ // falls back to libbitcoin's plain inputs/outputs form).
+ auto model = value_from(bitcoind(*tx));
+ inject_tx_context(model.as_object(), query, link);
+ send_result(rpc::value_t(std::move(model)),
+ two * tx->serialized_size(witness));
+ return true;
+}
+
+bool protocol_bitcoind_rpc::handle_send_raw_transaction(const code& ec,
+ rpc_interface::send_raw_transaction, const std::string& hexstring,
+ double) NOEXCEPT
+{
+ if (stopped(ec))
+ return false;
+
+ data_chunk data{};
+ if (!decode_base16(data, hexstring))
+ {
+ send_error(error::invalid_argument);
+ return true;
+ }
+
+ const auto tx = std::make_shared(data, true);
+ if (!tx->is_valid())
+ {
+ send_error(error::invalid_argument);
+ return true;
+ }
+
+ auto& query = archive();
+ const auto txid = tx->hash(false);
+
+ // Archive (so the out-relay can serve getdata) only if not already known.
+ // TODO: contextual validation (populate_with_metadata + connect) for policy.
+ if (query.to_tx(txid).is_terminal())
+ {
+ if (tx->check())
+ {
+ send_error(error::invalid_argument);
+ return true;
+ }
+
+ if (query.set_code(*tx))
+ {
+ send_error(database::error::integrity);
+ return true;
+ }
+ }
+
+ // Announce to peers; protocol_transaction_out_106 serves the tx on getdata.
+ broadcast(
+ std::make_shared(
+ messages::peer::transaction{ tx }));
+
+ send_result(encode_hash(txid), two * system::hash_size);
return true;
}
diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp
new file mode 100644
index 00000000..5267a4af
--- /dev/null
+++ b/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace server {
+
+using namespace system;
+
+uint32_t protocol_bitcoind_rpc::median_time_past(const node::query& query,
+ size_t height) NOEXCEPT
+{
+ constexpr size_t window = 11;
+ const auto count = std::min(window, height + 1u);
+ std::vector times{};
+ times.reserve(count);
+ for (size_t index = 0; index < count; ++index)
+ {
+ const auto header = query.get_header(query.to_confirmed(height - index));
+ if (header)
+ times.push_back(header->timestamp());
+ }
+
+ if (times.empty())
+ return 0;
+
+ std::sort(times.begin(), times.end());
+ return times.at(times.size() / 2u);
+}
+
+void protocol_bitcoind_rpc::inject_block_context(boost::json::object& out,
+ const node::query& query, const database::header_link& link,
+ const chain::header& header) NOEXCEPT
+{
+ size_t height{};
+ if (!query.get_height(height, link))
+ return;
+
+ const auto top = query.get_top_confirmed();
+ const auto confirmed = query.is_confirmed_block(link);
+ out["height"] = static_cast(height);
+ out["confirmations"] = confirmed ?
+ static_cast(top - height + 1u) : int64_t{ -1 };
+ out["mediantime"] = median_time_past(query, height);
+
+ if (header.previous_block_hash() != null_hash)
+ out["previousblockhash"] = encode_hash(header.previous_block_hash());
+
+ if (confirmed && height < top)
+ out["nextblockhash"] = encode_hash(
+ query.get_header_key(query.to_confirmed(height + 1u)));
+}
+
+void protocol_bitcoind_rpc::inject_tx_context(boost::json::object& out,
+ const node::query& query, const database::tx_link& link) NOEXCEPT
+{
+ size_t height{};
+ if (!query.is_confirmed_tx(link) || !query.get_tx_height(height, link))
+ {
+ out["confirmations"] = 0;
+ return;
+ }
+
+ const auto block = query.to_confirmed(height);
+ const auto top = query.get_top_confirmed();
+ const auto header = query.get_header(block);
+ out["in_active_chain"] = true;
+ out["blockhash"] = encode_hash(query.get_header_key(block));
+ out["confirmations"] = static_cast(top - height + 1u);
+ if (header)
+ {
+ out["blocktime"] = header->timestamp();
+ out["time"] = header->timestamp();
+ }
+}
+
+boost::json::object protocol_bitcoind_rpc::header_to_bitcoind(
+ const chain::header& header) NOEXCEPT
+{
+ return boost::json::object
+ {
+ { "hash", encode_hash(header.hash()) },
+ { "version", header.version() },
+ { "versionHex", encode_base16(to_big_endian(header.version())) },
+ { "merkleroot", encode_hash(header.merkle_root()) },
+ { "time", header.timestamp() },
+ { "nonce", header.nonce() },
+ { "bits", encode_base16(to_big_endian(header.bits())) },
+ { "difficulty", header.difficulty() }
+ };
+}
+
+std::string protocol_bitcoind_rpc::chain_name(const node::query& query) NOEXCEPT
+{
+ const auto genesis = query.get_header_key(query.to_confirmed(0));
+
+ using selection = chain::selection;
+ const std::pair networks[]
+ {
+ { selection::mainnet, "main" },
+ { selection::testnet, "test" },
+ { selection::regtest, "regtest" }
+ };
+
+ for (const auto& [network, name]: networks)
+ if (system::settings{ network }.genesis_block.hash() == genesis)
+ return name;
+
+ // Signet is not yet modeled in system::settings (stubbed by genesis hash).
+ if (encode_hash(genesis) ==
+ "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6")
+ return "signet";
+
+ return "unknown";
+}
+
+} // namespace server
+} // namespace libbitcoin
diff --git a/test/parsers/bitcoind_target.cpp b/test/parsers/bitcoind_target.cpp
index 8a2a0830..be23743a 100644
--- a/test/parsers/bitcoind_target.cpp
+++ b/test/parsers/bitcoind_target.cpp
@@ -20,9 +20,234 @@
BOOST_AUTO_TEST_SUITE(bitcoind_target_tests)
-BOOST_AUTO_TEST_CASE(bitcoind_target_test)
+using namespace system;
+using namespace network::rpc;
+using object_t = network::rpc::object_t;
+using media_type = network::http::media_type;
+
+static const std::string test_hash =
+ "0000000000000000000000000000000000000000000000000000000000000042";
+
+static const object_t& params_of(const request_t& request) NOEXCEPT
+{
+ BOOST_REQUIRE(request.params.has_value());
+ BOOST_REQUIRE(std::holds_alternative(request.params.value()));
+ return std::get(request.params.value());
+}
+
+static hash_cptr hash_of(const object_t& object) NOEXCEPT
+{
+ const auto& any = std::get(object.at("hash").value());
+ BOOST_REQUIRE(any.holds_alternative());
+ const auto cptr = any.get();
+ BOOST_REQUIRE(cptr);
+ return cptr;
+}
+
+static uint8_t media_of(const object_t& object) NOEXCEPT
+{
+ return std::get(object.at("media").value());
+}
+
+// general
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__valid__jsonrpc_v2_named_params)
+{
+ request_t out{};
+ BOOST_REQUIRE(!bitcoind_target(out, "/rest/chaininfo.json"));
+ BOOST_REQUIRE(out.jsonrpc == network::rpc::version::v2);
+ BOOST_REQUIRE(std::holds_alternative(out.params.value()));
+}
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__error_paths__expected)
+{
+ const std::vector> cases
+ {
+ { "", server::error::empty_path },
+ { "?foo=bar", server::error::empty_path },
+ { "/rest", server::error::missing_target },
+ { "/rest/bogus", server::error::invalid_target },
+ { "/bogus", server::error::invalid_target },
+ { "/rest/block", server::error::missing_hash },
+ { "/rest/block/" + test_hash, server::error::invalid_target },
+ { "/rest/block/" + test_hash + ".txt", server::error::invalid_target },
+ { "/rest/block/nothex.json", server::error::invalid_hash },
+ { "/rest/block/notxdetails", server::error::missing_hash },
+ { "/rest/block/spent", server::error::missing_hash },
+ { "/rest/blockhashbyheight", server::error::missing_target },
+ { "/rest/blockhashbyheight/abc.json", server::error::invalid_number },
+ { "/rest/blockhashbyheight/01.json", server::error::invalid_number },
+ { "/rest/headers", server::error::missing_target },
+ { "/rest/headers/abc/" + test_hash + ".json", server::error::invalid_number },
+ { "/rest/headers/3", server::error::missing_hash },
+ { "/rest/headers/3/nothex.json", server::error::invalid_hash },
+ { "/rest/blockfilter", server::error::missing_target },
+ { "/rest/blockfilter/extended/" + test_hash + ".json", server::error::invalid_target },
+ { "/rest/blockfilter/basic", server::error::missing_hash },
+ { "/rest/blockfilterheaders/basic", server::error::missing_hash },
+ { "/rest/blockpart", server::error::missing_hash },
+ { "/rest/blockpart/nothex/0/80.bin", server::error::invalid_hash },
+ { "/rest/blockpart/" + test_hash, server::error::missing_target },
+ { "/rest/blockpart/" + test_hash + "/abc/80.bin", server::error::invalid_number },
+ { "/rest/blockpart/" + test_hash + "/0", server::error::missing_target },
+ { "/rest/blockpart/" + test_hash + "/0/abc.bin", server::error::invalid_number }
+ };
+
+ for (const auto& [path, expected]: cases)
+ {
+ request_t out{};
+ BOOST_REQUIRE_MESSAGE(bitcoind_target(out, path) == expected, path);
+ }
+}
+
+// chaininfo
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__chaininfo_variants__chain_information)
+{
+ const std::vector paths
+ {
+ "/rest/chaininfo.json", "/rest/chaininfo", "/chaininfo.json", "chaininfo"
+ };
+
+ for (const auto& path: paths)
+ {
+ request_t out{};
+ BOOST_REQUIRE_MESSAGE(!bitcoind_target(out, path), path);
+ BOOST_REQUIRE_EQUAL(out.method, "chain_information");
+ BOOST_REQUIRE(params_of(out).empty());
+ }
+}
+
+// block
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__block_json__expected)
+{
+ request_t out{};
+ BOOST_REQUIRE(!bitcoind_target(out, "/rest/block/" + test_hash + ".json"));
+ BOOST_REQUIRE_EQUAL(out.method, "block");
+
+ const auto& object = params_of(out);
+ BOOST_REQUIRE_EQUAL(object.size(), 2u);
+ BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_json));
+ BOOST_REQUIRE_EQUAL(to_uintx(*hash_of(object)), uint256_t{ 0x42 });
+}
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__block_media__mapped)
{
- BOOST_REQUIRE(true);
+ const std::vector> cases
+ {
+ { "json", media_type::application_json },
+ { "hex", media_type::text_plain },
+ { "bin", media_type::application_octet_stream }
+ };
+
+ for (const auto& [extension, type]: cases)
+ {
+ request_t out{};
+ const auto path = "/rest/block/" + test_hash + "." + extension;
+ BOOST_REQUIRE_MESSAGE(!bitcoind_target(out, path), extension);
+ BOOST_REQUIRE_EQUAL(media_of(params_of(out)), to_value(type));
+ }
+}
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__block_notxdetails__block_txs)
+{
+ request_t out{};
+ const auto path = "/rest/block/notxdetails/" + test_hash + ".json";
+ BOOST_REQUIRE(!bitcoind_target(out, path));
+ BOOST_REQUIRE_EQUAL(out.method, "block_txs");
+
+ const auto& object = params_of(out);
+ BOOST_REQUIRE_EQUAL(object.size(), 2u);
+ BOOST_REQUIRE_EQUAL(to_uintx(*hash_of(object)), uint256_t{ 0x42 });
+}
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__block_spent__block_spent_tx_outputs)
+{
+ request_t out{};
+ const auto path = "/rest/block/spent/" + test_hash + ".json";
+ BOOST_REQUIRE(!bitcoind_target(out, path));
+ BOOST_REQUIRE_EQUAL(out.method, "block_spent_tx_outputs");
+ BOOST_REQUIRE_EQUAL(to_uintx(*hash_of(params_of(out))), uint256_t{ 0x42 });
+}
+
+// blockhashbyheight
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockhashbyheight__block_hash)
+{
+ request_t out{};
+ BOOST_REQUIRE(!bitcoind_target(out, "/rest/blockhashbyheight/5.json"));
+ BOOST_REQUIRE_EQUAL(out.method, "block_hash");
+
+ const auto& object = params_of(out);
+ BOOST_REQUIRE_EQUAL(object.size(), 2u);
+ BOOST_REQUIRE_EQUAL(std::get(object.at("height").value()), 5u);
+ BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_json));
+}
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockhashbyheight_zero__block_hash)
+{
+ request_t out{};
+ BOOST_REQUIRE(!bitcoind_target(out, "/rest/blockhashbyheight/0.json"));
+ BOOST_REQUIRE_EQUAL(out.method, "block_hash");
+ BOOST_REQUIRE_EQUAL(std::get(params_of(out).at("height").value()), 0u);
+}
+
+// headers
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__headers__block_headers)
+{
+ request_t out{};
+ const auto path = "/rest/headers/3/" + test_hash + ".json";
+ BOOST_REQUIRE(!bitcoind_target(out, path));
+ BOOST_REQUIRE_EQUAL(out.method, "block_headers");
+
+ const auto& object = params_of(out);
+ BOOST_REQUIRE_EQUAL(object.size(), 3u);
+ BOOST_REQUIRE_EQUAL(std::get(object.at("count").value()), 3u);
+ BOOST_REQUIRE_EQUAL(to_uintx(*hash_of(object)), uint256_t{ 0x42 });
+ BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_json));
+}
+
+// blockfilter
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilter_basic__block_filter)
+{
+ request_t out{};
+ const auto path = "/rest/blockfilter/basic/" + test_hash + ".json";
+ BOOST_REQUIRE(!bitcoind_target(out, path));
+ BOOST_REQUIRE_EQUAL(out.method, "block_filter");
+
+ const auto& object = params_of(out);
+ BOOST_REQUIRE_EQUAL(object.size(), 3u);
+ BOOST_REQUIRE_EQUAL(std::get(object.at("type").value()), 0u);
+ BOOST_REQUIRE_EQUAL(to_uintx(*hash_of(object)), uint256_t{ 0x42 });
+}
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilterheaders_basic__block_filter_headers)
+{
+ request_t out{};
+ const auto path = "/rest/blockfilterheaders/basic/" + test_hash + ".json";
+ BOOST_REQUIRE(!bitcoind_target(out, path));
+ BOOST_REQUIRE_EQUAL(out.method, "block_filter_headers");
+ BOOST_REQUIRE_EQUAL(std::get(params_of(out).at("type").value()), 0u);
+}
+
+// blockpart
+
+BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockpart__block_part)
+{
+ request_t out{};
+ const auto path = "/rest/blockpart/" + test_hash + "/0/80.bin";
+ BOOST_REQUIRE(!bitcoind_target(out, path));
+ BOOST_REQUIRE_EQUAL(out.method, "block_part");
+
+ const auto& object = params_of(out);
+ BOOST_REQUIRE_EQUAL(object.size(), 4u);
+ BOOST_REQUIRE_EQUAL(std::get(object.at("offset").value()), 0u);
+ BOOST_REQUIRE_EQUAL(std::get(object.at("size").value()), 80u);
+ BOOST_REQUIRE_EQUAL(to_uintx(*hash_of(object)), uint256_t{ 0x42 });
+ BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_octet_stream));
}
BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/bitcoind/bitcoind_json.cpp b/test/protocols/bitcoind/bitcoind_json.cpp
new file mode 100644
index 00000000..33bae17a
--- /dev/null
+++ b/test/protocols/bitcoind/bitcoind_json.cpp
@@ -0,0 +1,235 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include "../../test.hpp"
+#include "../../mocks/blocks.hpp"
+#include
+#include
+#include
+
+using namespace system;
+
+static std::string as_text(const boost::json::value& value) NOEXCEPT
+{
+ return { value.as_string().c_str() };
+}
+
+// Exposes the protected static json helpers for direct testing.
+struct json
+ : server::protocol_bitcoind_rpc
+{
+ using protocol_bitcoind_rpc::median_time_past;
+ using protocol_bitcoind_rpc::inject_block_context;
+ using protocol_bitcoind_rpc::inject_tx_context;
+ using protocol_bitcoind_rpc::header_to_bitcoind;
+ using protocol_bitcoind_rpc::chain_name;
+};
+
+// header_to_bitcoind
+// ----------------------------------------------------------------------------
+
+BOOST_AUTO_TEST_SUITE(bitcoind_header_to_bitcoind_tests)
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__header_to_bitcoind__block1_header__maps_fields)
+{
+ const auto& header = test::block1.header();
+ const auto out = json::header_to_bitcoind(header);
+
+ BOOST_REQUIRE_EQUAL(as_text(out.at("hash")), encode_hash(header.hash()));
+ BOOST_REQUIRE_EQUAL(out.at("version").to_number(), header.version());
+ BOOST_REQUIRE_EQUAL(as_text(out.at("versionHex")),
+ encode_base16(to_big_endian(header.version())));
+ BOOST_REQUIRE_EQUAL(as_text(out.at("merkleroot")),
+ encode_hash(header.merkle_root()));
+ BOOST_REQUIRE_EQUAL(out.at("time").to_number(), header.timestamp());
+ BOOST_REQUIRE_EQUAL(out.at("nonce").to_number(), header.nonce());
+ BOOST_REQUIRE_EQUAL(as_text(out.at("bits")),
+ encode_base16(to_big_endian(header.bits())));
+ BOOST_REQUIRE(out.at("difficulty").is_number());
+ BOOST_REQUIRE(!out.contains("height"));
+ BOOST_REQUIRE(!out.contains("confirmations"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+// ----------------------------------------------------------------------------
+
+struct bitcoind_json_setup_fixture
+{
+ DELETE_COPY_MOVE(bitcoind_json_setup_fixture);
+
+ bitcoind_json_setup_fixture()
+ : config_
+ {
+ system::chain::selection::mainnet,
+ test::web_pages,
+ test::web_pages
+ },
+ store_
+ {
+ [this]() NOEXCEPT -> const database::settings&
+ {
+ config_.database.path = TEST_DIRECTORY;
+ return config_.database;
+ }()
+ },
+ query_{ store_ }
+ {
+ BOOST_REQUIRE_MESSAGE(test::clear(test::directory), "json setup");
+ config_.database.interval_depth = 2;
+
+ const auto ec = store_.create([](auto, auto) {});
+ BOOST_REQUIRE_MESSAGE(!ec, ec.message());
+ BOOST_REQUIRE_MESSAGE(test::setup_ten_block_store(query_),
+ "json initialize");
+ }
+
+ ~bitcoind_json_setup_fixture()
+ {
+ const auto ec = store_.close([](auto, auto) {});
+ BOOST_WARN_MESSAGE(!ec, ec.message());
+ BOOST_WARN_MESSAGE(test::clear(test::directory), "json cleanup");
+ }
+
+ configuration config_;
+ test::store_t store_;
+ test::query_t query_;
+};
+
+BOOST_FIXTURE_TEST_SUITE(bitcoind_json_tests, bitcoind_json_setup_fixture)
+
+// chain_name
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__chain_name__mainnet_genesis__main)
+{
+ BOOST_REQUIRE_EQUAL(json::chain_name(query_), "main");
+}
+
+// median_time_past
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__median_time_past__genesis__genesis_time)
+{
+ BOOST_REQUIRE_EQUAL(json::median_time_past(query_, 0),
+ test::genesis.header().timestamp());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__median_time_past__height_nine__sorted_median)
+{
+ std::vector times
+ {
+ test::genesis.header().timestamp(),
+ test::block1.header().timestamp(),
+ test::block2.header().timestamp(),
+ test::block3.header().timestamp(),
+ test::block4.header().timestamp(),
+ test::block5.header().timestamp(),
+ test::block6.header().timestamp(),
+ test::block7.header().timestamp(),
+ test::block8.header().timestamp(),
+ test::block9.header().timestamp()
+ };
+
+ std::sort(times.begin(), times.end());
+ BOOST_REQUIRE_EQUAL(json::median_time_past(query_, 9),
+ times.at(times.size() / 2u));
+}
+
+// inject_block_context
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__middle__height_confirmations_siblings)
+{
+ const auto link = query_.to_header(test::block5_hash);
+ const auto header = query_.get_header(link);
+ BOOST_REQUIRE(header);
+
+ boost::json::object out{};
+ json::inject_block_context(out, query_, link, *header);
+
+ BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 5u);
+ BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 5);
+ BOOST_REQUIRE_EQUAL(out.at("mediantime").to_number(),
+ json::median_time_past(query_, 5));
+ BOOST_REQUIRE_EQUAL(as_text(out.at("previousblockhash")),
+ encode_hash(test::block4_hash));
+ BOOST_REQUIRE_EQUAL(as_text(out.at("nextblockhash")),
+ encode_hash(test::block6_hash));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__genesis__no_previous)
+{
+ const auto link = query_.to_header(test::block0_hash);
+ const auto header = query_.get_header(link);
+ BOOST_REQUIRE(header);
+
+ boost::json::object out{};
+ json::inject_block_context(out, query_, link, *header);
+
+ BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 0u);
+ BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 10);
+ BOOST_REQUIRE(!out.contains("previousblockhash"));
+ BOOST_REQUIRE_EQUAL(as_text(out.at("nextblockhash")),
+ encode_hash(test::block1_hash));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__tip__no_next)
+{
+ const auto link = query_.to_header(test::block9_hash);
+ const auto header = query_.get_header(link);
+ BOOST_REQUIRE(header);
+
+ boost::json::object out{};
+ json::inject_block_context(out, query_, link, *header);
+
+ BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 9u);
+ BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 1);
+ BOOST_REQUIRE_EQUAL(as_text(out.at("previousblockhash")),
+ encode_hash(test::block8_hash));
+ BOOST_REQUIRE(!out.contains("nextblockhash"));
+}
+
+// inject_tx_context
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__inject_tx_context__confirmed_coinbase__block_context)
+{
+ const auto txid = test::block1.transactions_ptr()->front()->hash(false);
+ const auto link = query_.to_tx(txid);
+
+ boost::json::object out{};
+ json::inject_tx_context(out, query_, link);
+
+ BOOST_REQUIRE(out.at("in_active_chain").as_bool());
+ BOOST_REQUIRE_EQUAL(as_text(out.at("blockhash")),
+ encode_hash(test::block1_hash));
+ BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 9);
+ BOOST_REQUIRE_EQUAL(out.at("blocktime").to_number(),
+ test::block1.header().timestamp());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_json__inject_tx_context__unknown__zero_confirmations)
+{
+ const auto link = query_.to_tx(null_hash);
+
+ boost::json::object out{};
+ json::inject_tx_context(out, query_, link);
+
+ BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 0);
+ BOOST_REQUIRE(!out.contains("blockhash"));
+ BOOST_REQUIRE(!out.contains("in_active_chain"));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/bitcoind/bitcoind_rest.cpp b/test/protocols/bitcoind/bitcoind_rest.cpp
index e2468146..e9404b50 100644
--- a/test/protocols/bitcoind/bitcoind_rest.cpp
+++ b/test/protocols/bitcoind/bitcoind_rest.cpp
@@ -19,6 +19,114 @@
#include "../../test.hpp"
#include "bitcoind_setup_fixture.hpp"
-BOOST_FIXTURE_TEST_SUITE(bitcoind_tests, bitcoind_ten_block_setup_fixture)
+using namespace system;
+
+static std::string as_text(const boost::json::value& value) NOEXCEPT
+{
+ return { value.as_string().c_str() };
+}
+
+namespace {
+
+const auto block0 = encode_hash(test::block0_hash);
+const auto block5 = encode_hash(test::block5_hash);
+const auto block9 = encode_hash(test::block9_hash);
+const auto header9 = encode_base16(test::header9_data);
+
+std::string block_hash_hex(const data_chunk& wire) NOEXCEPT
+{
+ return encode_hash(bitcoin_hash(chain::header::serialized_size(),
+ wire.data()));
+}
+
+} // namespace
+
+BOOST_FIXTURE_TEST_SUITE(bitcoind_rest_tests, bitcoind_ten_block_setup_fixture)
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__chaininfo_json__main_nine)
+{
+ const auto result = rest_json("/rest/chaininfo.json");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("chain")), "main");
+ BOOST_REQUIRE_EQUAL(result.at("blocks").as_int64(), 9);
+ BOOST_REQUIRE_EQUAL(as_text(result.at("bestblockhash")), block9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__block_json__block9_with_txs)
+{
+ const auto result = rest_json("/rest/block/" + block9 + ".json");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9);
+ BOOST_REQUIRE(result.at("tx").is_array());
+ BOOST_REQUIRE(result.at("tx").at(0).is_object());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__block_hex__hashes_to_block9)
+{
+ const auto hex = rest_text("/rest/block/" + block9 + ".hex");
+ data_chunk wire{};
+ BOOST_REQUIRE(decode_base16(wire, hex));
+ BOOST_REQUIRE_EQUAL(block_hash_hex(wire), block9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__block_bin__hashes_to_block9)
+{
+ const auto wire = rest_data("/rest/block/" + block9 + ".bin");
+ BOOST_REQUIRE_EQUAL(block_hash_hex(wire), block9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__block_notxdetails_json__txid_list)
+{
+ const auto result = rest_json("/rest/block/notxdetails/" + block9 + ".json");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9);
+ BOOST_REQUIRE(result.at("tx").is_array());
+ BOOST_REQUIRE(result.at("tx").at(0).is_string());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__block_spent_json__structured)
+{
+ const auto result = rest_json("/rest/block/spent/" + block9 + ".json");
+ BOOST_REQUIRE(result.is_array() || result.is_object());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__blockhashbyheight_json__height_five__block5)
+{
+ const auto result = rest_json("/rest/blockhashbyheight/5.json");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("blockhash")), block5);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__blockhashbyheight_json__genesis__block0)
+{
+ const auto result = rest_json("/rest/blockhashbyheight/0.json");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("blockhash")), block0);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_json__count_three_from_block5)
+{
+ const auto result = rest_json("/rest/headers/3/" + block5 + ".json");
+ BOOST_REQUIRE(result.is_array());
+ BOOST_REQUIRE_EQUAL(result.as_array().size(), 3u);
+ BOOST_REQUIRE_EQUAL(as_text(result.at(0).at("hash")), block5);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_hex__one_header__eighty_bytes)
+{
+ const auto hex = rest_text("/rest/headers/1/" + block9 + ".hex");
+ data_chunk wire{};
+ BOOST_REQUIRE(decode_base16(wire, hex));
+ BOOST_REQUIRE_EQUAL(wire.size(), 80u);
+ BOOST_REQUIRE_EQUAL(encode_base16(wire), header9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__blockpart_bin__block9_header)
+{
+ const auto wire = rest_data("/rest/blockpart/" + block9 + "/0/80.bin");
+ BOOST_REQUIRE_EQUAL(wire.size(), 80u);
+ BOOST_REQUIRE_EQUAL(encode_base16(wire), header9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rest__blockfilter_basic__filters_disabled__not_ok)
+{
+ const auto target = "/rest/blockfilter/basic/" + block9 + ".json";
+ BOOST_REQUIRE(rest_status(target) != boost::beast::http::status::ok);
+}
BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp
index e2468146..2eb7f99e 100644
--- a/test/protocols/bitcoind/bitcoind_rpc.cpp
+++ b/test/protocols/bitcoind/bitcoind_rpc.cpp
@@ -19,6 +19,233 @@
#include "../../test.hpp"
#include "bitcoind_setup_fixture.hpp"
+using namespace system;
+
+// Quote a hash as a single-element json-rpc params array.
+static std::string hash_param(const hash_digest& hash) NOEXCEPT
+{
+ return "[\"" + encode_hash(hash) + "\"]";
+}
+
+// Quote a hash plus a trailing scalar (e.g. verbosity) as a params array.
+static std::string hash_param(const hash_digest& hash,
+ const std::string& tail) NOEXCEPT
+{
+ return "[\"" + encode_hash(hash) + "\", " + tail + "]";
+}
+
+// True if the json-rpc response carries a non-null error member.
+static bool has_error(const boost::json::value& response) NOEXCEPT
+{
+ return response.is_object() && response.as_object().contains("error") &&
+ !response.at("error").is_null();
+}
+
+static std::string as_text(const boost::json::value& value) NOEXCEPT
+{
+ return std::string{ value.as_string().c_str() };
+}
+
+// The ten-block store contains mainnet blocks 0..9 (block9 is the tip).
BOOST_FIXTURE_TEST_SUITE(bitcoind_tests, bitcoind_ten_block_setup_fixture)
+// blockchain
+// ----------------------------------------------------------------------------
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockcount__ten_block_store__nine)
+{
+ const auto response = rpc("getblockcount");
+ BOOST_REQUIRE(response.at("result").is_int64());
+ BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getbestblockhash__ten_block_store__block9)
+{
+ const auto response = rpc("getbestblockhash");
+ BOOST_REQUIRE_EQUAL(as_text(response.at("result")),
+ encode_hash(test::block9_hash));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockhash__height_five__block5)
+{
+ const auto response = rpc("getblockhash", "[5]");
+ BOOST_REQUIRE_EQUAL(as_text(response.at("result")),
+ encode_hash(test::block5_hash));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockhash__genesis__block0)
+{
+ const auto response = rpc("getblockhash", "[0]");
+ BOOST_REQUIRE_EQUAL(as_text(response.at("result")),
+ encode_hash(test::block0_hash));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockheader__block9__no_transactions)
+{
+ const auto response = rpc("getblockheader", hash_param(test::block9_hash));
+ const auto& result = response.at("result");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("hash")),
+ encode_hash(test::block9_hash));
+ BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9);
+ BOOST_REQUIRE(!result.as_object().contains("tx"));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__block9_verbosity1__txid_list)
+{
+ const auto response = rpc("getblock", hash_param(test::block9_hash, "1"));
+ const auto& result = response.at("result");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("hash")),
+ encode_hash(test::block9_hash));
+ BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9);
+ BOOST_REQUIRE(result.at("tx").is_array());
+ BOOST_REQUIRE_EQUAL(result.at("tx").as_array().size(), 1u);
+ BOOST_REQUIRE(result.at("tx").at(0).is_string());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__block9_verbosity2__tx_objects)
+{
+ const auto response = rpc("getblock", hash_param(test::block9_hash, "2"));
+ const auto& tx = response.at("result").at("tx");
+ BOOST_REQUIRE(tx.is_array());
+ BOOST_REQUIRE(tx.at(0).is_object());
+ BOOST_REQUIRE(tx.at(0).as_object().contains("txid"));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected)
+{
+ const auto response = rpc("getblockchaininfo");
+ const auto& result = response.at("result");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("chain")), "main");
+ BOOST_REQUIRE_EQUAL(result.at("blocks").as_int64(), 9);
+ // The mock store populates the confirmed chain only; headers (candidate
+ // top) reflects that, so assert presence/type rather than a height.
+ BOOST_REQUIRE(result.at("headers").is_int64());
+ BOOST_REQUIRE_EQUAL(as_text(result.at("bestblockhash")),
+ encode_hash(test::block9_hash));
+
+ // Fields added alongside these tests.
+ BOOST_REQUIRE(result.as_object().contains("target"));
+ BOOST_REQUIRE(result.at("warnings").is_string());
+ BOOST_REQUIRE(!result.at("initialblockdownload").as_bool());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxout__unspent_coinbase__output)
+{
+ const auto txid = test::block1.transactions_ptr()->front()->hash(false);
+ const auto response = rpc("gettxout", hash_param(txid, "0"));
+ const auto& result = response.at("result");
+ BOOST_REQUIRE(result.is_object());
+ BOOST_REQUIRE(result.as_object().contains("value"));
+ BOOST_REQUIRE(result.as_object().contains("scriptPubKey"));
+}
+
+// rawtransactions
+// ----------------------------------------------------------------------------
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawtransaction__coinbase_raw__hex)
+{
+ const auto txid = test::block1.transactions_ptr()->front()->hash(false);
+ const auto response = rpc("getrawtransaction", hash_param(txid, "0"));
+ BOOST_REQUIRE(response.at("result").is_string());
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawtransaction__coinbase_verbose__context)
+{
+ const auto txid = test::block1.transactions_ptr()->front()->hash(false);
+ const auto response = rpc("getrawtransaction", hash_param(txid, "1"));
+ const auto& result = response.at("result");
+ BOOST_REQUIRE_EQUAL(as_text(result.at("txid")), encode_hash(txid));
+ BOOST_REQUIRE(result.at("vin").is_array());
+ BOOST_REQUIRE(result.at("vout").is_array());
+
+ // Chain context injected at the protocol layer.
+ BOOST_REQUIRE_EQUAL(as_text(result.at("blockhash")),
+ encode_hash(test::block1_hash));
+ BOOST_REQUIRE_EQUAL(result.at("confirmations").as_int64(), 9);
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawtransaction__unknown_txid__error)
+{
+ const auto response = rpc("getrawtransaction", hash_param(null_hash, "1"));
+ BOOST_REQUIRE(has_error(response));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__sendrawtransaction__invalid_hex__error)
+{
+ const auto response = rpc("sendrawtransaction", "[\"nothex\"]");
+ BOOST_REQUIRE(has_error(response));
+}
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__sendrawtransaction__malformed__error)
+{
+ const auto response = rpc("sendrawtransaction", "[\"00\"]");
+ BOOST_REQUIRE(has_error(response));
+}
+
+// network
+// ----------------------------------------------------------------------------
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkinfo__fields)
+{
+ const auto response = rpc("getnetworkinfo");
+ const auto& result = response.at("result");
+ BOOST_REQUIRE(result.as_object().contains("version"));
+ BOOST_REQUIRE(result.at("subversion").is_string());
+ BOOST_REQUIRE(result.as_object().contains("protocolversion"));
+ BOOST_REQUIRE(result.at("networks").is_array());
+}
+
+// declared but deliberately not implemented (structured not_implemented error)
+// ----------------------------------------------------------------------------
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__not_implemented__error)
+{
+ const std::vector> methods
+ {
+ { "getblockstats", "[0]" },
+ { "getchaintxstats", "[]" },
+ { "getchainwork", "[]" },
+ { "gettxoutsetinfo", "[]" },
+ { "scantxoutset", "[\"start\", []]" },
+ { "verifychain", "[]" },
+ { "verifytxoutset", "[\"test\"]" },
+ { "pruneblockchain", "[1]" },
+ { "savemempool", "[]" }
+ };
+
+ for (const auto& [method, params]: methods)
+ BOOST_REQUIRE_MESSAGE(has_error(rpc(method, params)), method);
+}
+
+// getblockfilter is implemented but requires bip158 (disabled in this store).
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockfilter__filters_disabled__error)
+{
+ const auto response = rpc("getblockfilter",
+ hash_param(test::block9_hash, "\"basic\""));
+ BOOST_REQUIRE(has_error(response));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+// segwit (witness store: genesis + two blocks carrying witness transactions)
+// ----------------------------------------------------------------------------
+
+BOOST_FIXTURE_TEST_SUITE(bitcoind_witness_tests, bitcoind_witness_setup_fixture)
+
+BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawtransaction__witness_tx__wtxid_differs)
+{
+ // block1a's first transaction carries witnesses; wtxid differs from txid.
+ const auto& tx = *test::block1a.transactions_ptr()->front();
+ const auto txid = tx.hash(false);
+ const auto response = rpc("getrawtransaction", hash_param(txid, "1"));
+ const auto& result = response.at("result");
+
+ BOOST_REQUIRE_EQUAL(as_text(result.at("txid")), encode_hash(txid));
+ BOOST_REQUIRE_NE(as_text(result.at("hash")), encode_hash(txid));
+
+ // Segwit weight accounting: vsize == ceil(weight / 4).
+ const auto weight = result.at("weight").as_int64();
+ BOOST_REQUIRE_EQUAL(result.at("vsize").as_int64(), (weight + 3) / 4);
+}
+
BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp
index 6178292c..0b446b90 100644
--- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp
+++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp
@@ -20,6 +20,26 @@
#include "../../mocks/blocks.hpp"
#include "bitcoind_setup_fixture.hpp"
#include
+#include
+
+using namespace boost::beast;
+
+namespace {
+
+// Internal linkage to avoid colliding with the native fixture's parse_json.
+boost::json::value parse_json(std::string_view value)
+{
+ try
+ {
+ return boost::json::parse(value);
+ }
+ catch (...)
+ {
+ return {};
+ }
+}
+
+} // namespace
bitcoind_setup_fixture::bitcoind_setup_fixture(const initializer& setup)
: config_
@@ -82,23 +102,101 @@ bitcoind_setup_fixture::~bitcoind_setup_fixture()
BOOST_WARN_MESSAGE(test::clear(test::directory), "bitcoind cleanup");
}
-boost::json::value bitcoind_setup_fixture::get(const std::string& request)
+bitcoind_setup_fixture::string_request
+bitcoind_setup_fixture::create_get(std::string_view target)
{
- socket_.send(boost::asio::buffer(request));
- boost::asio::streambuf stream{};
+ string_request request{ http::verb::get, target,
+ network::http::version_1_1 };
+ request.set(http::field::host, "localhost");
+ request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
+ request.keep_alive(true);
+ return request;
+}
- try
- {
- boost::asio::read_until(socket_, stream, '\n');
- }
- catch (const boost::system::system_error&)
- {
- ////BOOST_WARN_MESSAGE(false, e.what());
- return boost::json::parse(R"({"dropped":true})");
- }
+bitcoind_setup_fixture::string_request
+bitcoind_setup_fixture::create_post(std::string_view target,
+ std::string_view body)
+{
+ string_request request{ http::verb::post, target,
+ network::http::version_1_1 };
+ request.set(http::field::host, "localhost");
+ request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
+ request.set(http::field::content_type, "application/json");
+ request.body() = std::string{ body };
+ request.prepare_payload();
+ request.keep_alive(true);
+ return request;
+}
+
+boost::json::value bitcoind_setup_fixture::rpc(std::string_view method,
+ std::string_view params)
+{
+ std::ostringstream body{};
+ body << R"({"jsonrpc":"2.0","id":0,"method":")" << method
+ << R"(","params":)" << params << "}";
+ http::write(socket_, create_post("/", body.str()));
+
+ flat_buffer buffer{};
+ network::boost_code ec{};
+ http::response response{};
+ http::read(socket_, buffer, response, ec);
+ BOOST_CHECK_MESSAGE(!ec, ec.message());
+ return parse_json(response.body());
+}
+
+bitcoind_setup_fixture::status
+bitcoind_setup_fixture::rest_status(std::string_view target)
+{
+ http::write(socket_, create_get(target));
+
+ flat_buffer buffer{};
+ network::boost_code ec{};
+ http::response response{};
+ http::read(socket_, buffer, response, ec);
+ BOOST_CHECK_MESSAGE(!ec, ec.message());
+ return response.result();
+}
+
+boost::json::value bitcoind_setup_fixture::rest_json(std::string_view target)
+{
+ http::write(socket_, create_get(target));
+
+ flat_buffer buffer{};
+ network::boost_code ec{};
+ http::response response{};
+ http::read(socket_, buffer, response, ec);
+ BOOST_CHECK_MESSAGE(!ec, ec.message());
+ BOOST_CHECK_EQUAL(response.result(), http::status::ok);
+ return parse_json(response.body());
+}
+
+std::string bitcoind_setup_fixture::rest_text(std::string_view target)
+{
+ http::write(socket_, create_get(target));
+
+ flat_buffer buffer{};
+ network::boost_code ec{};
+ http::response response{};
+ http::read(socket_, buffer, response, ec);
+ BOOST_CHECK_MESSAGE(!ec, ec.message());
+ BOOST_CHECK_EQUAL(response.result(), http::status::ok);
+
+ auto body = response.body();
+ while (!body.empty() && (body.back() == '\n' || body.back() == '\r'))
+ body.pop_back();
+
+ return body;
+}
+
+system::data_chunk bitcoind_setup_fixture::rest_data(std::string_view target)
+{
+ http::write(socket_, create_get(target));
- std::string response{};
- std::istream response_stream{ &stream };
- std::getline(response_stream, response);
- return boost::json::parse(response);
+ flat_buffer buffer{};
+ network::boost_code ec{};
+ http::response response{};
+ http::read(socket_, buffer, response, ec);
+ BOOST_CHECK_MESSAGE(!ec, ec.message());
+ BOOST_CHECK_EQUAL(response.result(), http::status::ok);
+ return system::data_chunk(response.body().begin(), response.body().end());
}
diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp
index 1d8104ad..a346cf75 100644
--- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp
+++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp
@@ -24,18 +24,24 @@
#define BITCOIND_ENDPOINT "127.0.0.1:65000"
-// TODO: bitcoind is http so use boost::beast.
-
struct bitcoind_setup_fixture
{
- DELETE_COPY_MOVE(bitcoind_setup_fixture);
-
+ using status = boost::beast::http::status;
using initializer = std::function;
+
+ DELETE_COPY_MOVE(bitcoind_setup_fixture);
explicit bitcoind_setup_fixture(const initializer& setup);
~bitcoind_setup_fixture();
- // bitcoind does not implement any protocol version control or negotiation.
- boost::json::value get(const std::string& request);
+ // JSON-RPC 2.0 over HTTP POST to "/". params is a raw json value (array or
+ // object). Returns the parsed json-rpc response object (with result/error).
+ boost::json::value rpc(std::string_view method, std::string_view params="[]");
+
+ // Bitcoin Core REST over HTTP GET (target under "/rest/...").
+ status rest_status(std::string_view target);
+ boost::json::value rest_json(std::string_view target);
+ std::string rest_text(std::string_view target);
+ system::data_chunk rest_data(std::string_view target);
protected:
configuration config_;
@@ -43,10 +49,16 @@ struct bitcoind_setup_fixture
test::query_t query_;
private:
+ using string_body = network::http::string_body;
+ using string_request = boost::beast::http::request;
+ static string_request create_get(std::string_view target);
+ static string_request create_post(std::string_view target,
+ std::string_view body);
+
network::logger log_;
server::server_node server_;
boost::asio::io_context io{};
- boost::asio::ip::tcp::socket socket_{ io };
+ boost::beast::tcp_stream socket_{ io.get_executor() };
};
struct bitcoind_ten_block_setup_fixture
@@ -61,4 +73,16 @@ struct bitcoind_ten_block_setup_fixture
}
};
+struct bitcoind_witness_setup_fixture
+ : bitcoind_setup_fixture
+{
+ inline bitcoind_witness_setup_fixture()
+ : bitcoind_setup_fixture([](test::query_t& query)
+ {
+ return test::setup_three_block_witness_store(query);
+ })
+ {
+ }
+};
+
#endif