Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ docs/_build
/dist
/electrumx.egg-info
/e_x.egg-info
/venv
.vscode/
.mypy_cache/
.idea/
6 changes: 5 additions & 1 deletion electrumx/server/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
60 changes: 56 additions & 4 deletions electrumx/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Copy link
Contributor

Choose a reason for hiding this comment

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

What are these txids for? They are in reversed byte order versus normal RPC interface, so logging them isn't that helpful, because it's not like you can copy-paste them to an explorer to view the txns (unless you manually reverse them first).

Also, they are wtxids, and not normal txids, because you are hashing the raw txn which contains witness data, rather than the stripped txn...

Copy link
Member Author

@f321x f321x Oct 24, 2025

Choose a reason for hiding this comment

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

Thanks, good catch. Yeah this is incorrect i remove it in this followup PR #319

except ValueError:
self.logger.info(f"error calculating txids: {traceback.format_exc()}")
Comment on lines +1486 to +1487
Copy link
Member

Choose a reason for hiding this comment

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

no need to import traceback

Suggested change
except ValueError:
self.logger.info(f"error calculating txids: {traceback.format_exc()}")
except ValueError as e:
self.logger.info(f"error calculating txids", exc_info=e)

here is some boilerplate:

import traceback
import logging
import sys

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
console_stderr_handler = logging.StreamHandler(sys.stderr)
console_stderr_handler.setLevel(logging.DEBUG)
logger.addHandler(console_stderr_handler)

and now compare (in a fresh local python interpreter):

try:
    txids = [double_sha256(bytes.fromhex(tx)).hex() for tx in tx_package]
except Exception:
    logger.info(f"error calculating txids: {traceback.format_exc()}")

and

try:
    txids = [double_sha256(bytes.fromhex(tx)).hex() for tx in tx_package]
except Exception as e:
    logger.info(f"error calculating txids", exc_info=e)

and

try:
    txids = [double_sha256(bytes.fromhex(tx)).hex() for tx in tx_package]
except Exception:
    logger.info(f"error calculating txids", exc_info=True)

see https://docs.python.org/3/library/logging.html#logging.Logger.debug

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, that's useful to know!

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

Expand Down Expand Up @@ -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
Comment on lines +1609 to +1610
Copy link
Member

Choose a reason for hiding this comment

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

Let's either not expose the new method at all, or maybe expose it unconditionally by moving it ~6 lines higher into the big dict.

I don't want to gate it behind an arbitrary temporary version number. Once spesmilo/electrum-protocol#2 is done, we can gate it behind that version number along with the other changes.

Until it is documented in the spec, clients should not rely on this method existing at any version.

self.request_handlers = handlers


Expand Down
6 changes: 6 additions & 0 deletions tests/server/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down