From 7de390f0236e5f741644251e22cd8c8633cd4a00 Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 21 Jan 2025 14:37:37 +0100 Subject: [PATCH] implement submitpackage call --- .gitignore | 1 + electrumx/server/daemon.py | 6 +++- electrumx/server/session.py | 60 ++++++++++++++++++++++++++++++++++--- tests/server/test_daemon.py | 6 ++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index cceb134da..3e15a44f5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ docs/_build /dist /electrumx.egg-info /e_x.egg-info +/venv .vscode/ .mypy_cache/ .idea/ diff --git a/electrumx/server/daemon.py b/electrumx/server/daemon.py index dde3c3714..a2f0a785e 100644 --- a/electrumx/server/daemon.py +++ b/electrumx/server/daemon.py @@ -13,7 +13,7 @@ import time from calendar import timegm from struct import pack -from typing import TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Type, List import aiohttp from aiorpcx import JSONRPC @@ -310,6 +310,10 @@ async def broadcast_transaction(self, raw_tx): '''Broadcast a transaction to the network.''' return await self._send_single('sendrawtransaction', (raw_tx, )) + async def broadcast_package(self, raw_txs: List[str]): + """Broadcast a package of transactions to the network using 'submitpackage'.""" + return await self._send_single('submitpackage', (raw_txs, )) + async def height(self): '''Query the daemon for its current height.''' self._height = await self._send_single('getblockcount') diff --git a/electrumx/server/session.py b/electrumx/server/session.py index 75f83f3ee..2b3125d1d 100644 --- a/electrumx/server/session.py +++ b/electrumx/server/session.py @@ -15,10 +15,11 @@ import os import ssl import time +import traceback from collections import defaultdict from functools import partial from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, List import asyncio import attr @@ -32,7 +33,7 @@ from electrumx.lib.lrucache import LRUCache from electrumx.lib.util import OldTaskGroup from electrumx.lib.hash import (HASHX_LEN, Base58Error, hash_to_hex_str, - hex_str_to_hash, sha256) + hex_str_to_hash, sha256, double_sha256) from electrumx.lib.merkle import MerkleCache from electrumx.lib.text import sessions_lines from electrumx.server.daemon import DaemonError @@ -786,6 +787,11 @@ async def broadcast_transaction(self, raw_tx): self.txs_sent += 1 return hex_hash + async def broadcast_package(self, tx_package: List[str]) -> dict: + result = await self.daemon.broadcast_package(tx_package) + self.txs_sent += len(tx_package) + return result + async def limited_history(self, hashX): '''Returns a pair (history, cost). @@ -978,7 +984,7 @@ class ElectrumX(SessionBase): '''A TCP server that handles incoming Electrum connections.''' PROTOCOL_MIN = (1, 4) - PROTOCOL_MAX = (1, 4, 3) + PROTOCOL_MAX = (1, 4, 4) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1468,6 +1474,51 @@ async def transaction_broadcast(self, raw_tx): self.logger.info(f'sent tx: {hex_hash}') return hex_hash + async def package_broadcast(self, tx_package: List[str], verbose: bool = False) -> dict: + """Broadcast a package of raw transactions to the network (submitpackage). + The package must consist of a child with its parents, + and none of the parents may depend on one another. + + raw_txs: a list of raw transactions as hexadecimal strings""" + self.bump_cost(0.25 + sum(len(tx) / 5000 for tx in tx_package)) + try: + txids = [double_sha256(bytes.fromhex(tx)).hex() for tx in tx_package] + except ValueError: + self.logger.info(f"error calculating txids: {traceback.format_exc()}") + raise RPCError( + BAD_REQUEST, + f'not a valid hex encoded transaction package: {tx_package}') + try: + daemon_result = await self.session_mgr.broadcast_package(tx_package) + except DaemonError as e: + error, = e.args + message = error['message'] + self.logger.info(f"error submitting package: {message}") + raise RPCError(BAD_REQUEST, 'the tx package was rejected by ' + f'network rules.\n\n{message}. Package txids: {txids}') + else: + self.txs_sent += len(tx_package) + self.logger.info(f'broadcasted package: {txids}') + if verbose: + return daemon_result + errors = [] + for tx in daemon_result.get('tx-results', {}).values(): + if tx.get('error'): + error_msg = { + 'txid': tx.get('txid'), + 'error': tx['error'] + } + errors.append(error_msg) + # check both, package_msg and package-msg due to ongoing discussion to change rpc + # https://github.com/bitcoin/bitcoin/pull/31900 + package_msg = daemon_result.get('package_msg', daemon_result.get('package-msg')) + electrumx_result = { + 'success': True if package_msg == 'success' else False + } + if errors: + electrumx_result['errors'] = errors + return electrumx_result + async def transaction_get(self, tx_hash, verbose=False): '''Return the serialized raw transaction given its hash @@ -1555,7 +1606,8 @@ def set_request_handlers(self, ptuple): if ptuple >= (1, 4, 2): handlers['blockchain.scripthash.unsubscribe'] = self.scripthash_unsubscribe - + if ptuple >= (1, 4, 4): + handlers['blockchain.transaction.broadcast_package'] = self.package_broadcast self.request_handlers = handlers diff --git a/tests/server/test_daemon.py b/tests/server/test_daemon.py index 8bb065d4f..85c001b44 100644 --- a/tests/server/test_daemon.py +++ b/tests/server/test_daemon.py @@ -237,6 +237,12 @@ async def test_broadcast_transaction(daemon): daemon.session = ClientSessionGood(('sendrawtransaction', [raw_tx], tx_hash)) assert await daemon.broadcast_transaction(raw_tx) == tx_hash +@pytest.mark.asyncio +async def test_broadcast_package(daemon): + package = ["deadbeef", "deadc0de", "facefeed"] + result = {"package_msg": "success"} + daemon.session = ClientSessionGood(('submitpackage', [package], result)) + assert await daemon.broadcast_package(package) == result @pytest.mark.asyncio async def test_relayfee(daemon):