diff --git a/Model/main.py b/Model/main.py index a26cad4..ac567cc 100644 --- a/Model/main.py +++ b/Model/main.py @@ -14,15 +14,13 @@ HIST_CSV = os.getenv("HIST_CSV", None) RESERVE_STRAT = os.getenv("RESERVE_STRAT", None) ESTIMATE_STRAT = os.getenv("ESTIMATE_STRAT", None) -O_VERSION = os.getenv("O_VERSION", None) I_VERSION = os.getenv("I_VERSION", None) -ALLOCATE_VERSION = os.getenv("ALLOCATE_VERSION", None) CANCEL_COIN_SELECTION = os.getenv("CANCEL_COIN_SELECTION", None) -EXPECTED_ACTIVE_VAULTS = os.getenv("EXPECTED_ACTIVE_VAULTS", None) +NUMBER_VAULTS = os.getenv("NUMBER_VAULTS", None) REFILL_EXCESS = os.getenv("REFILL_EXCESS", None) REFILL_PERIOD = os.getenv("REFILL_PERIOD", None) -# Spend rate per day -SPEND_RATE = os.getenv("SPEND_RATE", None) +# Unvault rate per day +UNVAULT_RATE = os.getenv("UNVAULT_RATE", None) # Invalid rate per spend INVALID_SPEND_RATE = os.getenv("INVALID_SPEND_RATE", None) # Catastrophe rate per day @@ -42,24 +40,21 @@ HIST_CSV, RESERVE_STRAT, ESTIMATE_STRAT, - O_VERSION, I_VERSION, - ALLOCATE_VERSION, CANCEL_COIN_SELECTION, - EXPECTED_ACTIVE_VAULTS, + NUMBER_VAULTS, REFILL_EXCESS, REFILL_PERIOD, - SPEND_RATE, + UNVAULT_RATE, INVALID_SPEND_RATE, CATASTROPHE_RATE, ] if any(v is None for v in req_vars): logging.error( - "Need all these environment variables to be set: EXPECTED_ACTIVE_VAULTS," - " REFILL_EXCESS, REFILL_PERIOD, DELEGATION_PERIOD, INVALID_SPEND_RATE," - " CATASTROPHE_RATE, N_STK, N_MAN, LOCKTIME, HIST_CSV, RESERVE_STRAT," - " ESTIMATE_STRAT, O_VERSION, I_VERSION, ALLOCATE_VERSION," - " CANCEL_COIN_SELECTION." + "Need all these environment variables to be set: N_STK, N_MAN, LOCKTIME," + " HIST_CSV, RESERVE_STRAT, ESTIMATE_STRAT, I_VERSION," + " CANCEL_COIN_SELECTION, NUMBER_VAULTS, REFILL_EXCESS," + " REFILL_PERIOD, UNVAULT_RATE, INVALID_SPEND_RATE, CATASTROPHE_RATE." ) sys.exit(1) logging.info(f"Config: {', '.join(v for v in req_vars)}") @@ -70,21 +65,19 @@ HIST_CSV, RESERVE_STRAT, ESTIMATE_STRAT, - int(O_VERSION), int(I_VERSION), - int(ALLOCATE_VERSION), int(CANCEL_COIN_SELECTION), - int(EXPECTED_ACTIVE_VAULTS), + int(NUMBER_VAULTS), int(REFILL_EXCESS), int(REFILL_PERIOD), - float(SPEND_RATE), + float(UNVAULT_RATE), float(INVALID_SPEND_RATE), float(CATASTROPHE_RATE), with_balance=True, - with_fb_coins_dist=True, + # with_fb_coins_dist=True, with_cum_op_cost=True, with_divergence=True, - with_overpayments=True, + # with_overpayments=True, ) start_block = 350000 diff --git a/Model/plot_style.txt b/Model/plot_style.txt index c8310a2..5e26723 100644 --- a/Model/plot_style.txt +++ b/Model/plot_style.txt @@ -7,12 +7,12 @@ patch.force_edgecolor : True patch.linewidth: 0.8 scatter.edgecolors: black grid.color: b1afb5 -axes.titlesize: 16 +axes.titlesize: 13 legend.title_fontsize: 12 xtick.labelsize: 12 ytick.labelsize: 12 axes.labelsize: 12 -font.size: 10 +font.size: 12 # mathtext.fontset: stix # font.family: STIXGeneral lines.linewidth: 2 @@ -38,7 +38,7 @@ axes.linewidth: 0.6 axes.spines.right : False axes.spines.top : False axes.grid: True -figure.titlesize: 18 +figure.titlesize: 13 savefig.dpi: 300 # figure dots per inch or 'figure' savefig.facecolor: w # figure face color when saving diff --git a/Model/results.py b/Model/results.py index 3b5768a..9b39ed4 100644 --- a/Model/results.py +++ b/Model/results.py @@ -1,6 +1,7 @@ import logging import multiprocessing as mp import os +from pandas import DataFrame import pandas as pd import random import sys @@ -10,29 +11,27 @@ def sim_process(prng_seed, val=None, study_type=None, config_map=None): - logging.basicConfig(level=logging.INFO) + logging.basicConfig(level=logging.ERROR) req_types = [ "N_STK", "N_MAN", "HIST_CSV", "RESERVE_STRAT", "ESTIMATE_STRAT", - "O_VERSION", "I_VERSION", - "ALLOCATE_VERSION", - "EXPECTED_ACTIVE_VAULTS", + "NUMBER_VAULTS", "REFILL_PERIOD", "REFILL_EXCESS", - "SPEND_RATE", + "UNVAULT_RATE", "INVALID_SPEND_RATE", "CATASTROPHE_RATE", ] if study_type not in req_types: logging.error( - "Study requires a type from: EXPECTED_ACTIVE_VAULTS," - " REFILL_EXCESS, REFILL_PERIOD, REFILL_EXCESS, DELEGATION_PERIOD," + "Study requires a type from: NUMBER_VAULTS," + " REFILL_EXCESS, REFILL_PERIOD, REFILL_EXCESS, UNVAULT_RATE," " INVALID_SPEND_RATE, CATASTROPHE_RATE, N_STK, N_MAN, HIST_CSV," - " RESERVE_STRAT, ESTIMATE_STRAT, O_VERSION, I_VERSION, ALLOCATE_VERSION." + " RESERVE_STRAT, ESTIMATE_STRAT, I_VERSION." ) sys.exit(1) @@ -44,13 +43,11 @@ def sim_process(prng_seed, val=None, study_type=None, config_map=None): HIST_CSV = {config_map["HIST_CSV"]} RESERVE_STRAT = {config_map["RESERVE_STRAT"]} ESTIMATE_STRAT = {config_map["ESTIMATE_STRAT"]} - O_VERSION = {config_map["O_VERSION"]} I_VERSION = {config_map["I_VERSION"]} - ALLOCATE_VERSION = {config_map["ALLOCATE_VERSION"]} - EXPECTED_ACTIVE_VAULTS = {config_map["EXPECTED_ACTIVE_VAULTS"]} + NUMBER_VAULTS = {config_map["NUMBER_VAULTS"]} REFILL_PERIOD = {config_map["REFILL_PERIOD"]} REFILL_EXCESS = {config_map["REFILL_EXCESS"]} - SPEND_RATE = {config_map["SPEND_RATE"]} + UNVAULT_RATE = {config_map["UNVAULT_RATE"]} INVALID_SPEND_RATE = {config_map["INVALID_SPEND_RATE"]} CATASTROPHE_RATE = {config_map["CATASTROPHE_RATE"]} """ @@ -71,20 +68,18 @@ def sim_process(prng_seed, val=None, study_type=None, config_map=None): config_map["HIST_CSV"], config_map["RESERVE_STRAT"], config_map["ESTIMATE_STRAT"], - int(config_map["O_VERSION"]), int(config_map["I_VERSION"]), - int(config_map["ALLOCATE_VERSION"]), int(config_map["CANCEL_COIN_SELECTION"]), - int(config_map["EXPECTED_ACTIVE_VAULTS"]), - int(config_map["REFILL_EXCESS"] * config_map["EXPECTED_ACTIVE_VAULTS"]), + int(config_map["NUMBER_VAULTS"]), + int(config_map["REFILL_EXCESS"] * config_map["NUMBER_VAULTS"]), int(config_map["REFILL_PERIOD"]), - int(config_map["SPEND_RATE"]), + int(config_map["UNVAULT_RATE"]), float(config_map["INVALID_SPEND_RATE"]), float(config_map["CATASTROPHE_RATE"]), with_balance=True, with_divergence=True, with_cum_op_cost=True, - with_overpayments=True, + with_risk_status=True, ) try: sim.run(start_block, end_block) @@ -103,7 +98,6 @@ def multiprocess_run(range_seed, val, study_type, config_map): # FIXME: what if process fails? assert len(range_seed) >= 2 cores = len(range_seed) - # Todo, change the prng for each worker with mp.Pool(processes=cores) as pool: dfs = pool.map( partial(sim_process, val=val, study_type=study_type, config_map=config_map), @@ -122,25 +116,24 @@ def multiprocess_run(range_seed, val, study_type, config_map): config_map = { "N_STK": 5, "N_MAN": 3, - "LOCKTIME": 12, + "LOCKTIME": 72, "HIST_CSV": "../block_fees/historical_fees.csv", "RESERVE_STRAT": "CUMMAX95Q90", "ESTIMATE_STRAT": "ME30", - "O_VERSION": 1, - "I_VERSION": 2, - "ALLOCATE_VERSION": 0, - "EXPECTED_ACTIVE_VAULTS": 5, - "REFILL_PERIOD": 144 * 7, - "REFILL_EXCESS": 5, - "SPEND_RATE": 1, + "I_VERSION": 3, + "NUMBER_VAULTS": 5, + "REFILL_PERIOD": 144 * 31, + "REFILL_EXCESS": 1, + "UNVAULT_RATE": 1, + "DELEGATE_RATE": 1, "INVALID_SPEND_RATE": 0.1, "CATASTROPHE_RATE": 0.005, "CANCEL_COIN_SELECTION": 0, } # Set the study parameters - study_type = "O_VERSION" - val_range = [0, 1] + study_type = "NUMBER_VAULTS" + val_range = [1, 5, 10, 25, 50, 100, 500] sim_repeats = 10 cores = 10 @@ -148,6 +141,7 @@ def multiprocess_run(range_seed, val, study_type, config_map): range_seed = list(range(21000000, 21000000 + cores)) # Generate results + report_rows = [] for val in val_range: config_map[study_type] = val report = ( @@ -167,9 +161,50 @@ def multiprocess_run(range_seed, val, study_type, config_map): for df in sim_results: stats_df = pd.concat([stats_df, df], axis=0) + row = [val] for col in stats_df.columns: report += f"{col} mean: {stats_df[col].mean()}\n" report += f"{col} std dev: {stats_df[col].std()}\n" - + row.append(stats_df[col].mean()) + row.append(stats_df[col].std()) + report_rows.append(row) with open(f"{report_name}-{val}.txt", "w+", encoding="utf-8") as f: f.write(report) + + # Save the csv at each val in case of failure + report_df = DataFrame( + report_rows, + columns=[ + study_type, + "mean_balance_mean", + "mean_balance_std_dev", + "cum_ops_cost_mean", + "cum_ops_cost_std_dev", + "cum_cancel_fee_mean", + "cum_cancel_fee_std_dev", + "cum_cf_fee_mean", + "cum_cf_fee_std_dev", + "cum_refill_fee_mean", + "cum_refill_fee_std_dev", + "time_at_risk_mean", + "time_at_risk_std_dev", + "mean_recovery_time_mean", + "mean_recovery_time_std_dev", + "median_recovery_time_mean", + "median_recovery_time_std_dev", + "max_recovery_time_mean", + "max_recovery_time_std_dev", + "delegation_failure_count_mean", + "delegation_failure_count_std_dev", + "delegation_failure_rate_mean", + "delegation_failure_rate_std_dev", + "max_cancel_conf_time_mean", + "max_cancel_conf_time_std_dev", + "max_cf_conf_time_mean", + "max_cf_conf_time_std_dev", + "max_risk_coef_mean", + "max_risk_coef_std_dev", + ], + ) + report_df.set_index(f"{study_type}", inplace=True) + report_df.to_csv(f"{report_name}") diff --git a/Model/simulation.py b/Model/simulation.py index 958a631..d7d9090 100644 --- a/Model/simulation.py +++ b/Model/simulation.py @@ -37,14 +37,12 @@ def __init__( hist_feerate_csv, reserve_strat, estimate_strat, - o_version, i_version, - allocate_version, cancel_coin_selec, - exp_active_vaults, + num_vaults, refill_excess, refill_period, - spend_rate, + unvault_rate, invalid_spend_rate, catastrophe_rate, with_balance=False, @@ -59,11 +57,10 @@ def __init__( with_fb_coins_dist=False, ): # Stakeholder parameters - self.expected_active_vaults = exp_active_vaults - # In general 2 with reserve_strat = CUMMAX95Q90 and 10 to 15 with reserve_strat = 95Q90 + self.num_vaults = num_vaults self.refill_excess = refill_excess self.refill_period = refill_period - self.spend_rate = spend_rate + self.unvault_rate = unvault_rate # Manager parameters self.invalid_spend_rate = invalid_spend_rate @@ -77,9 +74,7 @@ def __init__( hist_feerate_csv, reserve_strat, estimate_strat, - o_version, i_version, - allocate_version, cancel_coin_selec, ) self.vault_count = 0 @@ -93,6 +88,7 @@ def __init__( self.with_op_cost = with_op_cost self.with_cum_op_cost = with_cum_op_cost self.costs = [] + self.wt_risk_time = [] self.with_overpayments = with_overpayments self.overpayments = [] self.with_coin_pool = with_coin_pool @@ -105,8 +101,6 @@ def __init__( self.risk_status = [] self.with_coin_pool_age = with_coin_pool_age self.coin_pool_age = [] - self.with_risk_time = with_risk_time - self.wt_risk_time = [] self.with_fb_coins_dist = with_fb_coins_dist self.fb_coins_dist = [] self.vm_values = [] @@ -120,14 +114,15 @@ def __init__( vb_coins_count: {self.wt.vb_coins_count}\n\ vm_factor: {self.wt.vm_factor}\n\ Refill excess: {self.refill_excess}\n\ - Expected active vaults: {self.expected_active_vaults}\n\ + Expected active vaults: {self.num_vaults}\n\ Refill period: {self.refill_period}\n\ - Spend rate: {self.spend_rate}\n\ + Unvault rate: {self.unvault_rate}\n\ Invalid spend rate: {self.invalid_spend_rate}\n\ Catastrophe rate: {self.catastrophe_rate}\n\ """ self.report_df = DataFrame( columns=[ + "mean_balance", "cum_ops_cost", "cum_cancel_fee", "cum_cf_fee", @@ -140,6 +135,7 @@ def __init__( "delegation_failure_rate", "max_cancel_conf_time", "max_cf_conf_time", + "max_risk_coef", ], index=[0], ) @@ -156,10 +152,9 @@ def required_reserve(self, block_height): def amount_needed(self, block_height, expected_new_vaults): """Returns amount to refill to ensure WT has sufficient operating balance. - Used by stakeholder wallet software. - R(t) in the paper. + Used by stakeholder wallet software. R(t, E) in the paper. - Note: stakeholder knows WT's balance and num_vaults (or expected_active_vaults). + Note: stakeholder knows WT's balance, num_vaults, fb_coins_dist. Stakeholder doesn't know which coins are allocated or not. """ bal = self.wt.balance() @@ -187,22 +182,30 @@ def amount_needed(self, block_height, expected_new_vaults): return int(R) def _reserve_divergence(self, block_height): + """Compute how far the vault's reserves have divereged from the current fee reserve per vault. + Compute the risk status; the total amount (satoshis) below the required reserve among available vaults.""" vaults = self.wt.list_available_vaults() if vaults != []: divergence = [] - frpv = self.wt.fee_reserve_per_vault(block_height) + required_reserve = sum(self.wt.coins_dist_reserve(block_height)) for vault in vaults: - div = vault.reserve_balance() - frpv + div = vault.reserve_balance() - required_reserve divergence.append(div) - # block, mean div, min div, max div - self.divergence.append( - [ - block_height, - sum(divergence) / len(vaults), - min(divergence), - max(divergence), - ] - ) + if self.with_divergence: + self.divergence.append( + [ + block_height, + sum(divergence) / len(vaults), + min(divergence), + max(divergence), + ] + ) + + if self.with_risk_status: + risk_by_vault = [div for div in divergence if div < 0] + nominal_risk = sum(risk_by_vault) + risk_coefficient = (len(risk_by_vault) / len(vaults)) * nominal_risk + self.risk_status.append((block_height, risk_coefficient)) def refill_sequence(self, block_height, expected_new_vaults): refill_amount = self.amount_needed(block_height, expected_new_vaults) @@ -280,14 +283,15 @@ def top_up_sequence(self, block_height): f" Allocation transition FAILED for vault {vault.id}: {str(e)}" ) - def spend_sequence(self, block_height): - logging.info(f"Spend sequence at block {block_height}") + def spend(self, block_height): if len(self.wt.list_available_vaults()) == 0: raise NoVaultToSpend vault_id = random.choice(self.wt.list_available_vaults()).id # Spend transition - logging.info(f" Spend transition at block {block_height}") + logging.info( + f" Spend transition with vault {vault_id} at block {block_height}" + ) self.wt.spend(vault_id, block_height) # snapshot coin pool after spend attempt @@ -295,8 +299,8 @@ def spend_sequence(self, block_height): amounts = [coin.amount for coin in self.wt.list_coins()] self.pool_after_spend.append([block_height, amounts]) - def cancel_sequence(self, block_height): - logging.info(f"Cancel sequence at block {block_height}") + def cancel(self, block_height): + logging.info(f"Cancel at block {block_height}") if len(self.wt.list_available_vaults()) == 0: raise NoVaultToSpend @@ -361,6 +365,9 @@ def confirm_sequence(self, height): + TX_OVERHEAD_SIZE <= MAX_TX_SIZE ) + logging.debug( + f" Consolidate-fanout confirm transition at block {height}" + ) self.wt.finalize_consolidate_fanout(tx, height) self.top_up_sequence(height) if tx.txouts[-1].processing_state == ProcessingState.UNPROCESSED: @@ -373,6 +380,7 @@ def confirm_sequence(self, height): self.cf_fee = 0 self.cf_fee += cf_fee elif isinstance(tx, CancelTx): + logging.debug(f" Cancel confirm transition at block {height}") self.wt.finalize_cancel(tx, height) else: raise @@ -387,10 +395,9 @@ def run(self, start_block, end_block): # At startup allocate as many reserves as we expect to have vaults logging.info( - f"Initializing at block {start_block} with {self.expected_active_vaults}" - " new vaults" + f"Initializing at block {start_block} with {self.num_vaults} new vaults" ) - self.refill_sequence(start_block, self.expected_active_vaults) + self.refill_sequence(start_block, self.num_vaults) # For each block in the range, simulate an action affecting the watchtower # (formally described as a sequence of transitions) based on the configured @@ -403,7 +410,7 @@ def run(self, start_block, end_block): # We always try to keep the number of expected vaults under watch. We might # not be able to allocate if a CF tx is pending but not yet confirmed. - for i in range(len(self.wt.list_vaults()), self.expected_active_vaults): + for i in range(len(self.wt.list_vaults()), self.num_vaults): amount = int(10e10) # 100 BTC try: self.wt.allocate(self.new_vault_id(), amount, block) @@ -419,19 +426,19 @@ def run(self, start_block, end_block): if block % self.refill_period == 0: self.refill_sequence(block, 0) - # The spend rate is a rate per day - if random.random() < self.spend_rate / BLOCKS_PER_DAY: + # The unvault rate is a rate per day + if random.random() < self.unvault_rate / BLOCKS_PER_DAY: self.delegate_sequence(block) # generate invalid spend, requires cancel if random.random() < self.invalid_spend_rate: try: - self.cancel_sequence(block) + self.cancel(block) except NoVaultToSpend: logging.info("Failed to Cancel, no vault to spend") # generate valid spend, requires processing else: try: - self.spend_sequence(block) + self.spend(block) except NoVaultToSpend: logging.info("Failed to Spend, no vault to spend") @@ -442,7 +449,7 @@ def run(self, start_block, end_block): except NoVaultToSpend: logging.info("Failed to Cancel (catastrophe), no vault to spend") # Reboot operation after catastrophe - self.refill_sequence(block, self.expected_active_vaults) + self.refill_sequence(block, self.num_vaults) if self.with_balance: self.balances.append( @@ -454,12 +461,6 @@ def run(self, start_block, end_block): ] ) - if self.with_risk_status: - status = self.wt.risk_status(block) - if (status["vaults_at_risk"] != 0) or ( - status["delegation_requires"] != 0 - ): - self.risk_status.append(status) if self.with_op_cost or self.with_cum_op_cost: self.costs.append( [block, self.refill_fee, self.cf_fee, self.cancel_fee] @@ -499,7 +500,7 @@ def run(self, start_block, end_block): risk_off = block self.wt_risk_time.append((risk_on, risk_off)) - if self.with_divergence: + if self.with_divergence or self.with_risk_status: self._reserve_divergence(block) if self.with_fb_coins_dist: @@ -553,7 +554,6 @@ def plot(self, output=None, show=False): self.with_coin_pool, self.with_risk_status, self.with_coin_pool_age, - self.with_risk_time, self.with_fb_coins_dist, ] ) @@ -566,12 +566,13 @@ def plot(self, output=None, show=False): if self.with_balance and self.balances != []: bal_df = DataFrame( self.balances, - columns=["block", "balance", "required reserve", "unallocated balance"], + columns=["block", "Balance", "Required Reserve", "Unallocated Balance"], ) bal_df.set_index(["block"], inplace=True) bal_df.plot(ax=axes[plot_num], title="WT Balance", legend=True) axes[plot_num].set_xlabel("Block", labelpad=15) axes[plot_num].set_ylabel("Satoshis", labelpad=15) + self.report_df["mean_balance"] = bal_df["Balance"].mean() plot_num += 1 costs_df = None @@ -770,39 +771,30 @@ def plot(self, output=None, show=False): ) div_df.set_index("Block", inplace=True) div_df["MeanDivergence"].plot( - ax=axes[plot_num], label="mean divergence", legend=True + ax=axes[plot_num], label="Mean Divergence", legend=True ) div_df["MinDivergence"].plot( - ax=axes[plot_num], label="minimum divergence", legend=True + ax=axes[plot_num], label="Minimum Divergence", legend=True ) div_df["MaxDivergence"].plot( - ax=axes[plot_num], label="max divergence", legend=True + ax=axes[plot_num], label="Max Divergence", legend=True ) axes[plot_num].set_xlabel("Block", labelpad=15) axes[plot_num].set_ylabel("Satoshis", labelpad=15) - axes[plot_num].set_title("Vault Divergence \nfrom Requirement") + axes[plot_num].set_title("Vault Reserve \n Divergence from Requirement") plot_num += 1 # Plot WT risk status if self.with_risk_status and self.risk_status != []: - risk_status_df = DataFrame(self.risk_status) - risk_status_df.set_index(["block"], inplace=True) - risk_status_df["num_vaults"].plot( - ax=axes[plot_num], label="number of vaults", color="r", legend=True + risk_status_df = DataFrame( + self.risk_status, columns=["block", "risk coefficient"] ) - risk_status_df["vaults_at_risk"].plot( - ax=axes[plot_num], label="vaults at risk", color="b", legend=True - ) - ax2 = axes[plot_num].twinx() - risk_status_df["delegation_requires"].plot( - ax=ax2, label="new delegation requires", color="g", legend=True - ) - risk_status_df["severity"].plot( - ax=ax2, label="total severity of risk", color="k", legend=True - ) - axes[plot_num].set_ylabel("Vaults", labelpad=15) + self.report_df["max_risk_coef"] = risk_status_df["risk coefficient"].max() + risk_status_df.set_index(["block"], inplace=True) + risk_status_df.plot(ax=axes[plot_num]) + axes[plot_num].set_title("Risk Coefficient, $\Omega$") + axes[plot_num].set_ylabel("Severity", labelpad=15) axes[plot_num].set_xlabel("Block", labelpad=15) - ax2.set_ylabel("Satoshis", labelpad=15) plot_num += 1 # Plot overpayments @@ -867,7 +859,7 @@ def plot(self, output=None, show=False): df = DataFrame(self.vb_values, columns=["Block", "Vb"]) df.set_index("Block", inplace=True) df.plot(ax=axes[plot_num], legend=True, color="blue") - axes[plot_num].legend(["$V_m$", "$V_b$"], loc="lower left") + axes[plot_num].legend(["$V_m$", "$V_b$"], loc="center right") plot_num += 1 @@ -881,41 +873,28 @@ def plot(self, output=None, show=False): if show: plt.show() - self.report_df["delegation_failure_count"][0] = self.delegation_failures - self.report_df["delegation_failure_rate"][0] = ( - self.delegation_failures / self.delegation_successes - ) - report += ( - f"Delegation failures: {self.delegation_failures} /" - f" {self.delegation_successes}" - f" ({self.delegation_failures / self.delegation_successes * 100}%)\n" - ) + if self.delegation_failures > 0 or self.delegation_successes > 0: + self.report_df["delegation_failure_count"][0] = self.delegation_failures + self.report_df["delegation_failure_rate"][0] = self.delegation_failures / ( + self.delegation_successes + self.delegation_failures + ) + + self.report_df["delegation_failure_rate"][0] = None + report += ( + f"Delegation failures: {self.delegation_failures} /" + f" { (self.delegation_successes + self.delegation_failures)}" + f" ({(self.delegation_failures / (self.delegation_successes + self.delegation_failures) )* 100}%)\n" + ) return (report, self.report_df) def plot_fee_history(self, start_block, end_block, output=None, show=False): plt.style.use(["plot_style.txt"]) - subplots_len = 3 - fig, axes = plt.subplots( - subplots_len, 1, sharex=True, figsize=(5.4, subplots_len * 3.9) - ) - self.wt.hist_df["mean_feerate"][start_block:end_block].plot(ax=axes[0]) - self.wt.hist_df["min_feerate"][start_block:end_block].plot( - ax=axes[1], legend=True - ) - self.wt.hist_df["max_feerate"][start_block:end_block].plot( - ax=axes[2], legend=True - ) - axes[0].set_title("Mean Fee Rate") - axes[0].set_ylabel("Satoshis", labelpad=15) - axes[0].set_xlabel("Block", labelpad=15) - axes[1].set_title("Min Fee Rate") - axes[1].set_ylabel("Satoshis", labelpad=15) - axes[1].set_xlabel("Block", labelpad=15) - axes[2].set_title("Max Fee Rate") - axes[2].set_ylabel("Satoshis", labelpad=15) - axes[2].set_xlabel("Block", labelpad=15) + fig, axes = plt.subplots(1, 1, figsize=(5.4, 3.9)) + self.wt.hist_df["mean_feerate"][start_block:end_block].plot(color="black") + axes.set_ylabel("Satoshis per Weight Unit", labelpad=15) + axes.set_xlabel("Block", labelpad=15) if output is not None: plt.savefig(f"{output}.png") @@ -974,7 +953,7 @@ def plot_fee_estimate( # FIXME: eventually have some small pytests if __name__ == "__main__": - # logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG) sim = Simulation( n_stk=5, n_man=5, @@ -982,24 +961,22 @@ def plot_fee_estimate( hist_feerate_csv="../block_fees/historical_fees.csv", reserve_strat="CUMMAX95Q90", estimate_strat="ME30", - o_version=1, - i_version=2, - allocate_version=1, + i_version=3, cancel_coin_selec=0, - exp_active_vaults=5, - refill_excess=4 * 5, + num_vaults=5, + refill_excess=0, refill_period=1008, - spend_rate=1, + unvault_rate=1, invalid_spend_rate=0.1, catastrophe_rate=0.05, - with_balance=False, - with_divergence=False, + with_balance=True, + with_divergence=True, with_op_cost=False, with_cum_op_cost=False, with_overpayments=False, with_coin_pool=False, with_coin_pool_age=False, - with_risk_status=False, + with_risk_status=True, with_risk_time=False, with_fb_coins_dist=False, ) @@ -1008,6 +985,7 @@ def plot_fee_estimate( end_block = 680000 sim.run(start_block, end_block) - sim.plot_frpv(start_block, end_block, show=True) - # sim.plot_fee_history(start_block, end_block, show=True) + # sim.plot(show=True) + # sim.plot_frpv(start_block, end_block, show=True) + # sim.plot_fee_history(start_block, end_block, output="fee_history") # sim.plot_fee_estimate("85Q1H", start_block, end_block, show=True) diff --git a/Model/statemachine.py b/Model/statemachine.py index 14dd5a6..dba4011 100644 --- a/Model/statemachine.py +++ b/Model/statemachine.py @@ -225,9 +225,7 @@ def __init__( hist_feerate_csv, reserve_strat, estimate_strat, - o_version, i_version, - allocate_version, cancel_coin_selec, ): self.n_stk = n_stk @@ -247,12 +245,10 @@ def __init__( # analysis strategy over historical feerates for Vm self.estimate_strat = estimate_strat - self.O_version = o_version self.I_version = i_version - self.allocate_version = allocate_version self.cancel_coin_selection = cancel_coin_selec - self.vb_coins_count = 8 + self.vb_coins_count = 6 self.vm_factor = 1.2 # multiplier M self.I_2_tol = 0.3 @@ -428,33 +424,8 @@ def coins_dist_reserve(self, block_height): These coins are needed to be able to Cancel up the reserve feerate, but not usually optimal during normal operations. """ - if self.O_version == 0: - vb = self.Vb(block_height) - return [vb] * self.vb_coins_count - - # Strategy 1 - # dist = [Vm, MVm, 2MVm, 3MVm, ...] - if self.O_version == 1: - reserve_feerate = self._feerate_reserve_per_vault(block_height) - fbcoin_cost = int(reserve_feerate * P2WPKH_INPUT_SIZE) - frpv = self.fee_reserve_per_vault(block_height) - Vm = self.Vm(block_height) - M = self.vm_factor # Factor increase per coin - dist = [Vm] - while sum(dist) < frpv - len(dist) * fbcoin_cost: - dist.append(int((len(dist)) * M * Vm + fbcoin_cost)) - diff = sum(dist) - frpv - int(len(dist) * fbcoin_cost) - # find the minimal subset sum of elements that is greater than diff, and remove them - subset = [] - while sum(subset) < diff: - subset.append(dist.pop()) - excess = sum(subset) - diff - assert isinstance(excess, int) - if excess >= Vm + fbcoin_cost: - dist.append(excess + fbcoin_cost) - else: - dist[-1] += excess - return dist + vb = self.Vb(block_height) + return [vb] * self.vb_coins_count def coins_dist_bonus(self, block_height): """The coin amount distribution used to reduce overpayments. @@ -701,6 +672,9 @@ def broadcast_consolidate_fanout(self, block_height): dist_bonu_size = P2WPKH_OUTPUT_SIZE * len(dist_bonus) dist_bonu_fees = int(dist_bonu_size * feerate) dist_bonu_cost = sum(dist_bonus) + dist_bonu_fees + # The cost of a change output should we need to add one + change_size = P2WPKH_OUTPUT_SIZE + change_fee = P2WPKH_OUTPUT_SIZE * feerate # Add new distributions of coins to the CF until we can't afford it anymore total_to_consume = sum(c.amount for c in coins) num_new_reserves = 0 @@ -716,12 +690,15 @@ def broadcast_consolidate_fanout(self, block_height): # Don't create a too large tx, instead add a change output (always # smaller than dist_rese_size) to be processed by a latter CF tx. if cf_size + dist_rese_size > MAX_TX_SIZE: + assert cf_size + change_size <= MAX_TX_SIZE, "No room for change output" added_coins.append( self.coin_pool.add_coin( - total_to_consume - consumed, + total_to_consume - consumed - change_fee, processing_state=ProcessingState.UNPROCESSED, ) ) + cf_size += change_size + cf_tx_fee += change_fee break consumed += dist_rese_cost cf_size += dist_rese_size @@ -742,12 +719,15 @@ def broadcast_consolidate_fanout(self, block_height): # Don't create a too large tx, instead add a change output (always # smaller than dist_rese_size) to be processed by a latter CF tx. if cf_size + dist_bonu_size > MAX_TX_SIZE: + assert cf_size + change_size <= MAX_TX_SIZE, "No room for change output" added_coins.append( self.coin_pool.add_coin( - total_to_consume - consumed, + total_to_consume - consumed - change_fee, processing_state=ProcessingState.UNPROCESSED, ) ) + cf_size += change_size + cf_tx_fee += change_fee break consumed += dist_bonu_cost cf_size += dist_bonu_size @@ -833,7 +813,7 @@ def finalize_consolidate_fanout(self, tx, height): return True return False - def _allocate_0(self, vault_id, amount, block_height): + def allocate(self, vault_id, amount, block_height): """WT allocates coins to a (new/existing) vault if there is enough available coins to meet the requirement. """ @@ -873,11 +853,7 @@ def _allocate_0(self, vault_id, amount, block_height): ] total_usable = sum(usable) required_reserve = sum(dist_req) - logging.debug( - f" Fee Reserve per Vault: {required_reserve}, " - f"Usable unallocated coins amounts: {usable} " - f"Unallocated coins: {self.coin_pool.unallocated_coins()}" - ) + if required_reserve > total_usable: raise AllocationError(required_reserve, total_usable) if remove_vault: @@ -902,7 +878,7 @@ def _allocate_0(self, vault_id, amount, block_height): self.allocate_coin(fbcoin, vault) logging.debug( f" {fbcoin} found with tolerance {tol*100}%, added to" - " fee reserve. Distribution value: {x}" + f" fee reserve. Distribution value: {x}" ) except (StopIteration): logging.debug( @@ -945,10 +921,6 @@ def _allocate_0(self, vault_id, amount, block_height): f" {vault.reserve_balance() - required_reserve}" ) - def allocate(self, vault_id, amount, block_height): - if self.allocate_version == 0: - self._allocate_0(vault_id, amount, block_height) - def broadcast_cancel(self, vault_id, block_height): """Construct and broadcast the cancel tx. @@ -961,30 +933,6 @@ def broadcast_cancel(self, vault_id, block_height): feerate = self.next_block_feerate(block_height) needed_fee = self.cancel_tx_fee(feerate, 0) - # FIXME: should we dropt that?? - # Strat 1: randomly select coins until the fee is met - # Performs moderately bad in low-stable fee market and ok in volatile fee market - # while init_fee > 0: - # coin = choice(vault['fee_reserve']) - # init_fee -= coin['amount'] - # cancel_fb_inputs.append(coin) - # vault['fee_reserve'].remove(coin) - # self.fbcoins.remove(coin) - # if vault['fee_reserve'] == []: - # raise RuntimeError(f"Fee reserve for vault {vault['id']} was insufficient to process cancel tx") - - # Strat 2: select smallest coins first - # FIXME: Performs bad in low-stable feemarket and good in volatile fee market - # while init_fee > 0: - # smallest_coin = min( - # vault['fee_reserve'], key=lambda coin: coin['amount']) - # cancel_fb_inputs.append(smallest_coin) - # vault['fee_reserve'].remove(smallest_coin) - # self.fbcoins.remove(smallest_coin) - # init_fee -= smallest_coin['amount'] - # if vault['fee_reserve'] == []: - # raise RuntimeError(f"Fee reserve for vault {vault['id']} was insufficient to process cancel tx") - cancel_fb_inputs = [] if self.cancel_coin_selection == 0: cancel_fb_inputs = self.cancel_coin_selec_0(vault, needed_fee, feerate) @@ -1157,30 +1105,6 @@ def spend(self, vault_id, height): """ self.remove_vault(self.vaults[vault_id]) - # TODO: re-think or get rid of - # def risk_status(self, block_height): - # """Return a summary of the risk status for the set of vaults being watched.""" - # # For cancel - # under_requirement = [] - # for _, vault in self.vaults: - # y = self.under_requirement(vault, block_height) - # if y != 0: - # under_requirement.append(y) - # # For delegation - # available = self.coin_pool.unallocated_coins() - # delegation_requires = sum(self.fb_coins_dist(block_height)) - sum( - # [coin.amount for coin in available] - # ) - # if delegation_requires < 0: - # delegation_requires = 0 - # return { - # "block": block_height, - # "num_vaults": len(self.vaults), - # "vaults_at_risk": len(under_requirement), - # "severity": sum(under_requirement), - # "delegation_requires": delegation_requires, - # } - # FIXME: eventually have some small pytests if __name__ == "__main__": @@ -1190,9 +1114,7 @@ def spend(self, vault_id, height): hist_feerate_csv="historical_fees.csv", reserve_strat="CUMMAX95Q90", estimate_strat="ME30", - o_version=1, i_version=2, - allocate_version=1, ) sm.refill(500000)