Skip to content

Commit 1f9c767

Browse files
author
f321x
committed
implement submitpackage call
remove venv implement submitpackage call add test, format log messages, improve docs implement submitpackage method fix linter error implement submitpackage call
1 parent f3013de commit 1f9c767

6 files changed

Lines changed: 112 additions & 4 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ docs/_build
1010
/dist
1111
/electrumx.egg-info
1212
/e_x.egg-info
13+
/venv
1314
.vscode/
1415
.mypy_cache/
1516
.idea/

docs/protocol-changes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,12 @@ New methods
175175
-----------
176176

177177
* :func:`blockchain.name.get_value_proof` to resolve a name (with proof). Name index coins (e.g. Namecoin) only.
178+
179+
180+
Version 1.4.4
181+
=============
182+
183+
New methods
184+
-----------
185+
186+
* :func:`blockchain.transaction.broadcast_package` to broadcast a package of transactions (submitpackage).

docs/protocol-methods.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,53 @@ Protocol version 1.0 returning an error as the result:
537537

538538
"258: txn-mempool-conflict"
539539

540+
blockchain.transaction.broadcast_package
541+
========================================
542+
543+
Broadcast a package of transactions to the network (submitpackage). The package must consist of a child with its parents,
544+
and none of the parents may depend on one another.
545+
546+
**Signature**
547+
548+
.. function:: blockchain.transaction.broadcast_package([raw_txs])
549+
550+
*[raw_txs]*
551+
552+
An array of raw transactions as hexadecimal strings.
553+
554+
**Result**
555+
556+
The Bitcoin Core daemon result according to its RPC API documentation.
557+
558+
**Result Example**
559+
560+
::
561+
562+
{ (json object)
563+
"package_msg" : "str", (string) The transaction package result message. "success" indicates all transactions were accepted into or are already in the mempool.
564+
"tx-results" : { (json object) transaction results keyed by wtxid
565+
"wtxid" : { (json object) transaction wtxid
566+
"txid" : "hex", (string) The transaction hash in hex
567+
"other-wtxid" : "hex", (string, optional) The wtxid of a different transaction with the same txid but different witness found in the mempool. This means the submitted transaction was ignored.
568+
"vsize" : n, (numeric, optional) Sigops-adjusted virtual transaction size.
569+
"fees" : { (json object, optional) Transaction fees
570+
"base" : n, (numeric) transaction fee in BTC
571+
"effective-feerate" : n, (numeric, optional) if the transaction was not already in the mempool, the effective feerate in BTC per KvB. For example, the package feerate and/or feerate with modified fees from prioritisetransaction.
572+
"effective-includes" : [ (json array, optional) if effective-feerate is provided, the wtxids of the transactions whose fees and vsizes are included in effective-feerate.
573+
"hex", (string) transaction wtxid in hex
574+
...
575+
]
576+
},
577+
"error" : "str" (string, optional) The transaction error string, if it was rejected by the mempool
578+
},
579+
...
580+
},
581+
"replaced-transactions" : [ (json array, optional) List of txids of replaced transactions
582+
"hex", (string) The transaction id
583+
...
584+
]
585+
}
586+
540587
blockchain.transaction.get
541588
==========================
542589

electrumx/server/daemon.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import time
1414
from calendar import timegm
1515
from struct import pack
16-
from typing import TYPE_CHECKING, Type
16+
from typing import TYPE_CHECKING, Type, List
1717

1818
import aiohttp
1919
from aiorpcx import JSONRPC
@@ -310,6 +310,10 @@ async def broadcast_transaction(self, raw_tx):
310310
'''Broadcast a transaction to the network.'''
311311
return await self._send_single('sendrawtransaction', (raw_tx, ))
312312

313+
async def broadcast_package(self, raw_txs: List[str]):
314+
"""Broadcast a package of transactions to the network using 'submitpackage'."""
315+
return await self._send_single('submitpackage', (raw_txs, ))
316+
313317
async def height(self):
314318
'''Query the daemon for its current height.'''
315319
self._height = await self._send_single('getblockcount')

electrumx/server/session.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
import os
1616
import ssl
1717
import time
18+
import traceback
1819
from collections import defaultdict
1920
from functools import partial
2021
from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network
21-
from typing import Optional, TYPE_CHECKING
22+
from typing import Optional, TYPE_CHECKING, List
2223
import asyncio
2324

2425
import attr
@@ -786,6 +787,11 @@ async def broadcast_transaction(self, raw_tx):
786787
self.txs_sent += 1
787788
return hex_hash
788789

790+
async def broadcast_package(self, tx_package: List[str]) -> dict:
791+
result = await self.daemon.broadcast_package(tx_package)
792+
self.txs_sent += len(tx_package)
793+
return result
794+
789795
async def limited_history(self, hashX):
790796
'''Returns a pair (history, cost).
791797
@@ -978,7 +984,7 @@ class ElectrumX(SessionBase):
978984
'''A TCP server that handles incoming Electrum connections.'''
979985

980986
PROTOCOL_MIN = (1, 4)
981-
PROTOCOL_MAX = (1, 4, 3)
987+
PROTOCOL_MAX = (1, 4, 4)
982988

983989
def __init__(self, *args, **kwargs):
984990
super().__init__(*args, **kwargs)
@@ -1468,6 +1474,40 @@ async def transaction_broadcast(self, raw_tx):
14681474
self.logger.info(f'sent tx: {hex_hash}')
14691475
return hex_hash
14701476

1477+
async def package_broadcast(self, tx_package: List[str]):
1478+
"""Broadcast a package of raw transactions to the network (submitpackage).
1479+
The package must consist of a child with its parents,
1480+
and none of the parents may depend on one another.
1481+
1482+
raw_txs: a list of raw transactions as hexadecimal strings"""
1483+
self.bump_cost(0.25 + sum(len(tx) / 5000 for tx in tx_package))
1484+
try:
1485+
txids = [sha256(bytes.fromhex(tx)).hex() for tx in tx_package]
1486+
result = await self.session_mgr.broadcast_package(tx_package)
1487+
except ValueError:
1488+
self.logger.info(f"error calculating txids: {traceback.format_exc()}")
1489+
raise RPCError(
1490+
BAD_REQUEST,
1491+
f'not a valid hex encoded transaction package: {tx_package}')
1492+
except DaemonError as e:
1493+
error, = e.args
1494+
message = error['message']
1495+
self.logger.info(f"error submitting package: {message}")
1496+
raise RPCError(BAD_REQUEST, 'the tx package was rejected by '
1497+
f'network rules.\n\n{message}. Package txids: {txids}')
1498+
else:
1499+
self.txs_sent += len(tx_package)
1500+
client_ver = util.protocol_tuple(self.client)
1501+
if client_ver != (0, ):
1502+
msg = self.coin.warn_old_client_on_tx_broadcast(client_ver)
1503+
if msg:
1504+
self.logger.info(f'sent package: {txids}. and warned user to upgrade their '
1505+
f'client from {self.client}')
1506+
return msg
1507+
1508+
self.logger.info(f'broadcasted package: {txids}')
1509+
return result
1510+
14711511
async def transaction_get(self, tx_hash, verbose=False):
14721512
'''Return the serialized raw transaction given its hash
14731513
@@ -1555,7 +1595,8 @@ def set_request_handlers(self, ptuple):
15551595

15561596
if ptuple >= (1, 4, 2):
15571597
handlers['blockchain.scripthash.unsubscribe'] = self.scripthash_unsubscribe
1558-
1598+
if ptuple >= (1, 4, 4):
1599+
handlers['blockchain.transaction.broadcast_package'] = self.package_broadcast
15591600
self.request_handlers = handlers
15601601

15611602

tests/server/test_daemon.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ async def test_broadcast_transaction(daemon):
237237
daemon.session = ClientSessionGood(('sendrawtransaction', [raw_tx], tx_hash))
238238
assert await daemon.broadcast_transaction(raw_tx) == tx_hash
239239

240+
@pytest.mark.asyncio
241+
async def test_broadcast_package(daemon):
242+
package = ["deadbeef", "deadc0de", "facefeed"]
243+
result = {"package_msg": "success"}
244+
daemon.session = ClientSessionGood(('submitpackage', [package], result))
245+
assert await daemon.broadcast_package(package) == result
240246

241247
@pytest.mark.asyncio
242248
async def test_relayfee(daemon):

0 commit comments

Comments
 (0)