Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/electrumx/server/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ async def getrawtransaction(self, txid_hum: str, verbose=False):
return await self._send_single('getrawtransaction',
(txid_hum, int(verbose)))

async def getblockstats(self, height: int):
'''Return per-block statistics from the daemon for the given height.'''
return await self._send_single('getblockstats', (height,))

async def getrawtransactions(self, txids_hum: Iterable[str], replace_errs=True) -> Sequence[bytes | None]:
'''Return the serialized raw transactions with the given hashes.

Expand Down
10 changes: 10 additions & 0 deletions src/electrumx/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1924,6 +1924,15 @@ async def _merkle_proof(self, cp_height: int, height: int) -> dict[str, Any]:
'root': hash_to_hex_str(root),
}

async def phandle_block_stats(self, height: int | Any):
'''Return per-block statistics from the daemon for the given height.

height: the block height as a non-negative integer
'''
height = non_negative_integer(height)
self.bump_cost(1.0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] bump_cost(1.0) for getblockstats — same tariff as relayfee. Bitcoind scans the whole block; your rate limiter thinks it's a nap. Cheap spam vector for expensive daemon work.

return await self.daemon_request('getblockstats', height)

async def phandle_block_header(self, height: int, cp_height: int = 0) -> dict[str, Any]:
'''Return a raw block header as a hexadecimal string, or as a
dictionary with a merkle proof.'''
Expand Down Expand Up @@ -2371,6 +2380,7 @@ def set_request_handlers(self, ptuple):
handlers = {
'blockchain.block.header': self.phandle_block_header,
'blockchain.block.headers': self.phandle_block_headers,
'blockchain.block.stats': self.phandle_block_stats,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Handler lives on base ElectrumX for every coin. Dash/Legacy nodes without getblockstats get mystery daemon errors instead of a clean 'unsupported'. Bitcoin-only fork, multi-coin footgun.

'blockchain.estimatefee': self.phandle_estimatefee,
'blockchain.headers.subscribe': self.phandle_headers_subscribe,
'blockchain.transaction.broadcast': self.phandle_transaction_broadcast,
Expand Down
35 changes: 35 additions & 0 deletions tests/server/test_api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import asyncio
from unittest import mock

import pytest
from aiorpcx import RPCError
from electrumx import Controller, Env
from electrumx.server.session import ElectrumX

loop = asyncio.new_event_loop()

Expand Down Expand Up @@ -43,6 +45,39 @@ def ensure_text_exception(test, exception):
def test_dummy():
assert True


@pytest.mark.asyncio
async def test_phandle_block_stats():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Congratulations, subject: you invented a mirror. Mock returns payload, handler returns payload, test applauds. Zero behavior verified beyond 'Python can call async functions.' Cake optional.

session = mock.Mock()
session.bump_cost = mock.Mock()
payload = {'height': 800000, 'avgfee': 1234}
session.daemon_request = mock.AsyncMock(return_value=payload)

result = await ElectrumX.phandle_block_stats(session, 800000)

assert result == payload
session.bump_cost.assert_called_once_with(1.0)
session.daemon_request.assert_awaited_once_with('getblockstats', 800000)


@pytest.mark.asyncio
async def test_phandle_block_stats_rejects_bad_height():
session = mock.Mock()
session.bump_cost = mock.Mock()
session.daemon_request = mock.AsyncMock()

with pytest.raises(RPCError):
await ElectrumX.phandle_block_stats(session, -1)

session.daemon_request.assert_not_called()


def test_block_stats_handler_registered():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] You assigned a handler to a dict, then tested the dict remembers. Even my potato cores could pass this. Registration tautology is not an experiment.

session = ElectrumX.__new__(ElectrumX)
session.set_request_handlers((1, 4))
assert session.request_handlers['blockchain.block.stats'] == session.phandle_block_stats


def _test_transaction_get():
async def test_verbose_ignore_by_backend():
env = set_env()
Expand Down
8 changes: 8 additions & 0 deletions tests/server/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ async def test_getrawtransaction(daemon):
hex_hash, True) == verbose


@pytest.mark.asyncio
async def test_getblockstats(daemon):
height = 800000
result = {'height': height, 'avgfee': 1234, 'total_size': 999}
daemon.session = ClientSessionGood(('getblockstats', [height], result))
assert await daemon.getblockstats(height) == result


@pytest.mark.asyncio
async def test_protx(dash_daemon):
protx_hash = 'deadbeaf'
Expand Down