An earlier version of this document used the poission distribution of the average block interval rahter than the exponential, which led to innacurate results. A discussion of this error can be found here. The error in the code has been corrected in the following article, but the diagrams still need to be re-generated.
The below simulation results show that OP_CHECKTEMPLATEVERIFY is an effective too to reduce network strain in Bitcoin. This is based on the same simulation used in the BIP.
The chart shows how the Bitcoin Mempool will respond during two periods of intense transaction demand. During the first period, rates go back to zero. In the second period, rates remain at the max transactions per second threshold.
The last chart shows that the Mempool is much less congested when using OP_CHECKTEMPLATEVERIFY than without it.
This section walks through the code to show how the simulation was generated.
These are some basic parameters. They can be modified to change the behavior of
the simulation. The most important parameters to consider are
which informs us what percent of demand can go into OP_CHECKTEMPLATEVERIFY.
import numpy as np import matplotlib.pyplot as plt PHASES = 50 PHASE_LENGTH = 1 SAMPLES = PHASE_LENGTH * PHASES AVG_TX = 235 MAX_BLOCK_SIZE = 1e6 AVG_INTERVAL = 10.0*60.0 TXNS_PER_SEC = 0.5*MAX_BLOCK_SIZE/AVG_TX/AVG_INTERVAL COMPRESSABLE = 0.50 BRANCHING_FACTOR = 4.0 EXTRA_WORK_MULTIPLIER = 1/(1-1.0/BRANCHING_FACTOR) DAYS = np.array(range(SAMPLES))/144.0 DAYS_OFFSET = PHASES/144.0*PHASE_LENGTH def show_legend(for_): plt.legend(for_, [l.get_label() for l in for_], loc='upper left') plt.rcParams["font.size"] = "31"
get_rate tells our simulation how to generate the curve of transactions per second.
Modifying it would allow you to simulate different demand curves.
def generate_rates(phase): if phase >= 20: return TXNS_PER_SEC elif phase > 10: return 1.25**(20 - phase) *TXNS_PER_SEC elif phase >= 5: return 1.25**(phase-5)*TXNS_PER_SEC else: return TXNS_PER_SEC rates = [generate_rates(phase) for phase in range(PHASES/2)] + [generate_rates(phase)*2 for phase in range(PHASES/2)] def get_rate(phase): return rates[phase] plt.subplot(3,1,1) p = 100*COMPRESSABLE plt.title("Transaction Compression Performance with %d%% Adoption During 2 Spikes"%p) plt.ylabel("Txn/s") p_full_block, = plt.plot([DAYS, DAYS[-1]], [MAX_BLOCK_SIZE/AVG_TX/AVG_INTERVAL]*2, label="Maximum Average Transactions Per Second") T = np.array(range(0, SAMPLES, PHASE_LENGTH))/144.0 p5, = plt.plot(T, rates, "k-", label="Transactions Per Second") show_legend([p5, p_full_block])
compressed() simulates a network running where a portion of users use OP_CHECKTEMPLATEVERIFY.
def compressed(compressable = COMPRESSABLE): backlog = 0 postponed_backlog = 0 # reserve space for results results_confirmed = *SAMPLES results_unconfirmed = *SAMPLES results_yet_to_spend = *SAMPLES total_time = *(SAMPLES) for phase in range(PHASES): for sample_idx in range(PHASE_LENGTH*phase, PHASE_LENGTH*(1+phase)): # Sample how much time until the next block total_time[sample_idx] = np.random.exponential(AVG_INTERVAL) # Random sample a number of transactions that occur in this block time period # Equivalent to the sum of one poisson per block time # I.E., \sum_1_n Pois(a) = Pois(a*n) txns = np.random.poisson(get_rate(phase)*total_time[sample_idx]) postponed = txns * compressable # Add the non postponed transactions to the backlog as the available weight weight = (txns-postponed)*AVG_TX + backlog # Add the postponed to the postponed_backlog postponed_backlog += postponed*AVG_TX * EXTRA_WORK_MULTIPLIER # Total extra work # If we have more weight available than block space if weight > MAX_BLOCK_SIZE: # clear what we can -- 1 MAX_BLOCK_SIZE results_confirmed[sample_idx] += MAX_BLOCK_SIZE backlog = weight - MAX_BLOCK_SIZE else: # Otherwise, we have some space to spare for postponed backlog space = MAX_BLOCK_SIZE - weight postponed_backlog = max(postponed_backlog-space, 0) backlog = 0 # record results in terms of transactions results_unconfirmed[sample_idx] = float(backlog)/AVG_TX results_yet_to_spend[sample_idx] = postponed_backlog/EXTRA_WORK_MULTIPLIER/AVG_TX return results_unconfirmed, results_yet_to_spend, np.cumsum(total_time)/(60*60*24.0) compressed_txs, unspendable, blocktimes_c = compressed()
normal() simulates a network running where no users use OP_CHECKTEMPLATEVERIFY.
def normal(): a,_,b = compressed(0) return a,b normal_txs, blocktimes_n = normal()
Now we just glue the data together to display it.
plt.subplot(3,1,2) plt.ylabel("Pending Txns") p7, = plt.plot(blocktimes_c1, unspendable, label="Confirmed Congestion Control Pending") show_legend([p7]) plt.subplot(3,1,3) plt.ylabel("Mempool Txns") p1, = plt.plot(blocktimes_n, normal_txs, label="Unconfirmed Mempool without Congestion Control") p3, = plt.plot(blocktimes_c1, compressed_txs, label="Unconfirmed Mempool with Congestion Control") show_legend([p1, p3]) plt.xlabel("Block Days") plt.show()
Simulations are cheap facsimiles of reality; they does not generally accurately model the universe. Even simulations generated by replaying real past events do not predict what will happen in the future.
This simulation in particular makes some strong assumptions about miner behavior. Miners always fill blocks as much as possible. In actuality, miners often mine blocks which aren’t full even when there is a backlog of transactions.
This simulation does not make any attempt at estimating the fees savings. Fees may be non-linearly correlated with the mempool length, or they may be wholly unaffected depending on consumer behavior, which can be measured empirically, but does not necessarily predict future behavior. A good example of why past data may not be predictive is that people may care to have transactions confirmed much more in response to political news.
This simulation also does not address Jevons Paradox – whereby by making a system more efficient to use, net consumption is increased, rather than decreased. Such market questions are beyond the scope of the simulation.
Fortunately, for many of the mismatches between expectation and reality this
simulation is somewhat robust as a comparative measure. Unless miners are
specifically discriminating against
OP_CHECKTEMPLATEVERIFY transactions, what the
results demonstrate is that
OP_CHECKTEMPLATEVERIFY will perform better than the
corresponding behavior that would be seen without
Thus, we can show that
OP_CHECKTEMPLATEVERIFY conveys a benefit across a wide variety of
scenarios. For example, replacing
rates = [np.random.poisson(7) for _ in range(PHASES/4)] + [np.random.poisson(14) for _ in range(PHASES/4)] + [np.random.poisson(3) for _ in range(PHASES/2)] def get_rate(phase): return rates[phase]
The simulation framework can also show smaller adoption cases, e.g.
COMPRESSABLE = 0.04. In this scenario we see the global mempool does not
benefit as much, but the 4% of users who adopt
greatly by not having to compete with the mempool.