Skip to content

Commit 2d7403f

Browse files
ecdsaNeil
authored andcommitted
New protocol: (#330)
- add method mempool.get_fee_histogram - bump protocol version to 1.2
1 parent 1da936a commit 2d7403f

File tree

5 files changed

+84
-14
lines changed

5 files changed

+84
-14
lines changed

docs/PROTOCOL.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,20 @@ Subscribe to a script hash.
774774
[**scripthash**, **status**]
775775

776776

777+
mempool.get_fee_histogram
778+
=========================
779+
780+
Return a histogram of the fee rates paid by transactions in the memory
781+
pool, weighted by transaction size.
782+
783+
The histogram is an array of (fee, vsize) values, where vsize_n is the
784+
cumulative virtual size of mempool transactions with a fee rate in the
785+
interval [fee_(n-1), fee_n)], and fee_(n-1) > fee_n.
786+
787+
Fee intervals may have variable size. The choice of appropriate
788+
intervals is currently not part of the protocol.
789+
790+
777791
server.add_peer
778792
===============
779793

server/controller.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,14 @@ async def estimatefee(self, number):
825825
number = self.non_negative_integer(number)
826826
return await self.daemon_request('estimatefee', [number])
827827

828+
def mempool_get_fee_histogram(self):
829+
'''Memory pool fee histogram.
830+
831+
TODO: The server should detect and discount transactions that
832+
never get mined when they should.
833+
'''
834+
return self.mempool.get_fee_histogram()
835+
828836
async def relayfee(self):
829837
'''The minimum fee a low-priority tx must pay in order to be accepted
830838
to the daemon's memory pool.'''

server/mempool.py

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class MemPool(util.LoggedClass):
2525
2626
To that end we maintain the following maps:
2727
28-
tx_hash -> (txin_pairs, txout_pairs)
28+
tx_hash -> (txin_pairs, txout_pairs, tx_fee, tx_size)
2929
hashX -> set of all tx hashes in which the hashX appears
3030
3131
A pair is a (hashX, value) tuple. tx hashes are hex strings.
@@ -42,6 +42,9 @@ def __init__(self, bp, controller):
4242
self.txs = {}
4343
self.hashXs = defaultdict(set) # None can be a key
4444
self.synchronized_event = asyncio.Event()
45+
self.fee_histogram = defaultdict(int)
46+
self.compact_fee_histogram = []
47+
self.histogram_time = 0
4548

4649
def _resync_daemon_hashes(self, unprocessed, unfetched):
4750
'''Re-sync self.txs with the list of hashes in the daemon's mempool.
@@ -52,6 +55,7 @@ def _resync_daemon_hashes(self, unprocessed, unfetched):
5255
txs = self.txs
5356
hashXs = self.hashXs
5457
touched = self.touched
58+
fee_hist = self.fee_histogram
5559

5660
hashes = self.daemon.cached_mempool_hashes()
5761
gone = set(txs).difference(hashes)
@@ -60,7 +64,11 @@ def _resync_daemon_hashes(self, unprocessed, unfetched):
6064
unprocessed.pop(hex_hash, None)
6165
item = txs.pop(hex_hash)
6266
if item:
63-
txin_pairs, txout_pairs = item
67+
txin_pairs, txout_pairs, tx_fee, tx_size = item
68+
fee_rate = tx_fee // tx_size
69+
fee_hist[fee_rate] -= tx_size
70+
if fee_hist[fee_rate] == 0:
71+
fee_hist.pop(fee_rate)
6472
tx_hashXs = set(hashX for hashX, value in txin_pairs)
6573
tx_hashXs.update(hashX for hashX, value in txout_pairs)
6674
for hashX in tx_hashXs:
@@ -138,6 +146,7 @@ async def main_loop(self):
138146
def _async_process_some(self, limit):
139147
pending = []
140148
txs = self.txs
149+
fee_hist = self.fee_histogram
141150

142151
async def process(unprocessed):
143152
nonlocal pending
@@ -160,10 +169,13 @@ async def process(unprocessed):
160169
pending.extend(deferred)
161170
hashXs = self.hashXs
162171
touched = self.touched
163-
for hex_hash, in_out_pairs in result.items():
172+
for hex_hash, item in result.items():
164173
if hex_hash in txs:
165-
txs[hex_hash] = in_out_pairs
166-
for hashX, value in itertools.chain(*in_out_pairs):
174+
txs[hex_hash] = item
175+
txin_pairs, txout_pairs, tx_fee, tx_size = item
176+
fee_rate = tx_fee // tx_size
177+
fee_hist[fee_rate] += tx_size
178+
for hashX, value in itertools.chain(txin_pairs, txout_pairs):
167179
touched.add(hashX)
168180
hashXs[hashX].add(hex_hash)
169181

@@ -209,7 +221,7 @@ def process_raw_txs(self, raw_tx_map, pending):
209221
for tx_hash, raw_tx in raw_tx_map.items():
210222
if tx_hash not in txs:
211223
continue
212-
tx = deserializer(raw_tx).read_tx()
224+
tx, tx_size = deserializer(raw_tx).read_tx_and_vsize()
213225

214226
# Convert the tx outputs into (hashX, value) pairs
215227
txout_pairs = [(script_hashX(txout.pk_script), txout.value)
@@ -219,7 +231,7 @@ def process_raw_txs(self, raw_tx_map, pending):
219231
txin_pairs = [(hash_to_str(txin.prev_hash), txin.prev_idx)
220232
for txin in tx.inputs]
221233

222-
pending.append((tx_hash, txin_pairs, txout_pairs))
234+
pending.append((tx_hash, txin_pairs, txout_pairs, tx_size))
223235

224236
# Now process what we can
225237
result = {}
@@ -229,7 +241,7 @@ def process_raw_txs(self, raw_tx_map, pending):
229241
if self.stop:
230242
break
231243

232-
tx_hash, old_txin_pairs, txout_pairs = item
244+
tx_hash, old_txin_pairs, txout_pairs, tx_size = item
233245
if tx_hash not in txs:
234246
continue
235247

@@ -259,7 +271,10 @@ def process_raw_txs(self, raw_tx_map, pending):
259271
if mempool_missing:
260272
deferred.append(item)
261273
else:
262-
result[tx_hash] = (txin_pairs, txout_pairs)
274+
# Compute fee
275+
tx_fee = (sum(v for hashX, v in txin_pairs) -
276+
sum(v for hashX, v in txout_pairs))
277+
result[tx_hash] = (txin_pairs, txout_pairs, tx_fee, tx_size)
263278

264279
return result, deferred
265280

@@ -290,9 +305,7 @@ async def transactions(self, hashX):
290305
item = self.txs.get(hex_hash)
291306
if not item or not raw_tx:
292307
continue
293-
txin_pairs, txout_pairs = item
294-
tx_fee = (sum(v for hashX, v in txin_pairs) -
295-
sum(v for hashX, v in txout_pairs))
308+
txin_pairs, txout_pairs, tx_fee, tx_size = item
296309
tx = deserializer(raw_tx).read_tx()
297310
unconfirmed = any(hash_to_str(txin.prev_hash) in self.txs
298311
for txin in tx.inputs)
@@ -325,7 +338,36 @@ def value(self, hashX):
325338
# hashXs is a defaultdict
326339
if hashX in self.hashXs:
327340
for hex_hash in self.hashXs[hashX]:
328-
txin_pairs, txout_pairs = self.txs[hex_hash]
341+
txin_pairs, txout_pairs, tx_fee, tx_size = self.txs[hex_hash]
329342
value -= sum(v for h168, v in txin_pairs if h168 == hashX)
330343
value += sum(v for h168, v in txout_pairs if h168 == hashX)
331344
return value
345+
346+
def get_fee_histogram(self):
347+
now = time.time()
348+
if now > self.histogram_time + 30:
349+
self.update_compact_histogram()
350+
self.histogram_time = now
351+
return self.compact_fee_histogram
352+
353+
def update_compact_histogram(self):
354+
# For efficiency, get_fees returns a compact histogram with
355+
# variable bin size. The compact histogram is an array of
356+
# (fee, vsize) values. vsize_n is the cumulative virtual size
357+
# of mempool transactions with a fee rate in the interval
358+
# [fee_(n-1), fee_n)], and fee_(n-1) > fee_n. Fee intervals
359+
# are chosen so as to create tranches that contain at least
360+
# 100kb of transactions
361+
l = list(reversed(sorted(self.fee_histogram.items())))
362+
out = []
363+
size = 0
364+
r = 0
365+
binsize = 100000
366+
for fee, s in l:
367+
size += s
368+
if size + r > binsize:
369+
out.append((fee, size))
370+
r += size - binsize
371+
size = 0
372+
binsize *= 1.1
373+
self.compact_fee_histogram = out

server/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ def set_protocol_handlers(self, ptuple):
449449
'blockchain.transaction.get': controller.transaction_get,
450450
})
451451

452+
if ptuple >= (1, 2):
453+
# New handler as of 1.2
454+
handlers.update({
455+
'mempool.get_fee_histogram': controller.mempool_get_fee_histogram,
456+
})
457+
452458
self.electrumx_handlers = handlers
453459

454460
def request_handler(self, method):

server/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
VERSION = 'ElectrumX 1.2.1'
44
PROTOCOL_MIN = '0.9'
5-
PROTOCOL_MAX = '1.1'
5+
PROTOCOL_MAX = '1.2'

0 commit comments

Comments
 (0)