def _script_safe_json(data) -> str:
return json.dumps(data, allow_nan=False).replace("</", "<\\/")
PLOTLY_PRESETS = {name: asdict(params) for name, params in PARAMETER_PRESETS.items()}
PLOTLY_DEFAULTS = {
"gas_units_per_proof": DEFAULT_GAS_UNITS_PER_PROOF,
"infra_cost_per_proof_usd": DEFAULT_INFRA_COST_PER_PROOF_USD,
"eth_usd": DEFAULT_ETH_USD,
"gas_price_gwei": DEFAULT_GAS_PRICE_GWEI,
"aztc_usd": DEFAULT_AZTC_USD,
"epoch_reward_pool_aztc": DEFAULT_EPOCH_REWARD_POOL_AZTC,
"active_full_weight_provers": DEFAULT_ACTIVE_FULL_WEIGHT_PROVERS,
"epoch_minutes": DEFAULT_EPOCH_MINUTES,
}
def render_preset_comparison_plotly_controls():
display(Markdown("### Side-by-side recovery and cost summary"))
html = """
<div class="aztec-plotly-widget" id="aztec-comparison-widget">
<div class="aztec-control-grid">
<label>Missed epochs <input id="aztec-comparison-missed" type="range" min="1" max="20" step="1" value="1" /></label>
<output id="aztec-comparison-missed-value">1</output>
<label>Active provers <input id="aztec-comparison-active-provers" type="number" min="1" step="1" /></label>
<label>Epoch reward AZTC <input id="aztec-comparison-reward-pool" type="number" min="0" step="100" /></label>
<label>AZTC/USD <input id="aztec-comparison-aztc-usd" type="number" min="0" step="0.001" /></label>
<label>ETH/USD <input id="aztec-comparison-eth-usd" type="number" min="0" step="100" /></label>
<label>Gas gwei <input id="aztec-comparison-gas-gwei" type="number" min="0" step="0.1" /></label>
<label>Gas units/proof <input id="aztec-comparison-gas-units" type="number" min="0" step="1000" /></label>
<label>Infra $/proof <input id="aztec-comparison-infra-cost" type="number" min="0" step="1" /></label>
</div>
<div id="aztec-comparison-summary" class="aztec-summary-table"></div>
<div id="aztec-comparison-plot-status" class="aztec-plot-status"></div>
<div id="aztec-comparison-plot-title" class="aztec-plot-title"></div>
<div id="aztec-comparison-plot" class="aztec-panel-grid aztec-comparison-grid">
<section class="aztec-panel"><h4>Recovery path</h4><div id="aztec-comparison-recovery-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-recovery-legend" class="aztec-panel-legend"></div></section>
<section class="aztec-panel"><h4>Gross cash spend<br>to reach full weight</h4><div id="aztec-comparison-cash-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-cash-legend" class="aztec-panel-legend"></div></section>
<section class="aztec-panel"><h4>Penalty cliff</h4><div id="aztec-comparison-penalty-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-penalty-legend" class="aztec-panel-legend"></div></section>
<section class="aztec-panel"><h4>Full-weight profitability</h4><div id="aztec-comparison-profit-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-profit-legend" class="aztec-panel-legend"></div></section>
</div>
</div>
<style>
.aztec-plotly-widget { width: 100%; }
.aztec-control-grid { align-items: end; display: grid; gap: 10px 12px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); margin: 10px 0 14px; }
.aztec-control-grid label { display: grid; font-size: 13px; font-weight: 600; gap: 3px; }
.aztec-control-grid input, .aztec-control-grid select { box-sizing: border-box; min-width: 0; width: 100%; }
.aztec-control-grid input[type="range"] { min-width: 150px; }
.aztec-control-grid output { align-self: center; font-weight: 600; }
.aztec-summary-table { overflow-x: auto; margin-bottom: 12px; }
.aztec-summary-table table { border-collapse: collapse; font-size: 13px; min-width: 920px; width: 100%; }
.aztec-summary-table th, .aztec-summary-table td { border: 1px solid #d1d5db; padding: 6px 8px; text-align: left; vertical-align: top; }
.aztec-summary-table th { background: #f3f4f6; }
.aztec-plot-status { color: #4b5563; font-size: 13px; margin: 6px 0; }
.aztec-plot-title { font-size: 18px; font-weight: 700; margin: 10px 0 10px; text-align: center; }
.aztec-panel-grid { display: grid; gap: 18px 22px; margin-top: 8px; }
.aztec-comparison-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.aztec-panel { min-width: 0; }
.aztec-panel h4 { font-size: 20px; font-weight: 700; line-height: 1.15; margin: 0 0 8px; min-height: 50px; text-align: center; }
.aztec-panel-plot { height: 300px; width: 100%; }
.aztec-panel-legend { align-items: center; display: flex; flex-wrap: wrap; gap: 4px 12px; justify-content: center; min-height: 20px; padding-top: 2px; }
.aztec-legend-item { align-items: center; color: #374151; display: inline-flex; font-size: 12px; gap: 5px; line-height: 1.2; white-space: nowrap; }
.aztec-legend-line { border-top: 2px solid var(--legend-color); display: inline-block; flex: 0 0 auto; width: 22px; }
.aztec-legend-line.dash { border-top-style: dashed; }
.aztec-legend-line.dot { border-top-style: dotted; }
@media (max-width: 820px) { .aztec-comparison-grid { grid-template-columns: 1fr; } }
</style>
<script src="__PLOTLY_CDN__"></script>
<script>
(() => {
const PRESETS = __PRESETS__;
const DEFAULTS = __DEFAULTS__;
const COLORS = {
"Current / original": "#1f77b4",
"AZIP-5 proposed": "#ff7f0e",
"htimsk softer revision": "#2ca02c",
"Custom": "#9467bd"
};
const comparisonColumns = ["Parameter set", "First return", "Recovery proofs", "Recovery time", "Reward loss", "Gross cash", "Break-even"];
const summaryColumns = ["Metric", "Value"];
const pathColumns = ["event", "timeline_epoch", "proof_index_after_return", "score", "relative_weight", "reward_usd", "cash_spend_usd", "profit_usd", "reward_opportunity_loss_usd", "cumulative_cash_spend_usd", "cumulative_reward_opportunity_loss_usd"];
function byId(id) { return document.getElementById(id); }
function num(id) { return Number(byId(id).value); }
function setVal(id, value) { const el = byId(id); if (el) el.value = value; }
function finite(value) { return Number.isFinite(value); }
function linspace(start, stop, count) {
if (count <= 1) return [start];
const step = (stop - start) / (count - 1);
return Array.from({length: count}, (_, i) => start + step * i);
}
function intRange(start, stopInclusive) {
return Array.from({length: Math.max(0, stopInclusive - start + 1)}, (_, i) => start + i);
}
function fmtNumber(value, decimals = 2, prefix = "", suffix = "") {
if (value === null || value === undefined || Number.isNaN(value)) return "";
if (value === Infinity) return "inf";
if (value === -Infinity) return "-inf";
return `${prefix}${Number(value).toLocaleString(undefined, {minimumFractionDigits: decimals, maximumFractionDigits: decimals})}${suffix}`;
}
function fmtInt(value) {
if (value === null || value === undefined || Number.isNaN(value)) return "";
if (!finite(value)) return value > 0 ? "inf" : "-inf";
return Math.round(value).toLocaleString();
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function renderTable(target, columns, rows) {
const head = columns.map(column => `<th>${escapeHtml(column)}</th>`).join("");
const body = rows.map(row => `<tr>${columns.map(column => `<td>${escapeHtml(row[column])}</td>`).join("")}</tr>`).join("");
target.innerHTML = `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
}
function cashCost(gasUnits, gasGwei, ethUsd, infraUsd) {
return gasUnits * gasGwei * 1e-9 * ethUsd + infraUsd;
}
function maintenanceCoverage(params) {
if (params.increment <= 0) return Infinity;
return Math.min(1, params.decay_rate / params.increment);
}
function shareFromScore(score, params) {
const capped = Math.min(Math.max(score, 0), params.max_score);
const raw = params.k - params.a * Math.pow(params.max_score - capped, 2) / 1e10;
return Math.max(raw, params.minimum);
}
function successfulUpdate(priorScore, elapsedEpochs, params) {
const decayed = Math.max(0, priorScore - params.decay_rate * elapsedEpochs);
return Math.min(params.max_score, decayed + params.increment);
}
function rewardFraction(proverShare, params, activeProvers) {
const active = Math.max(Math.round(activeProvers), 1);
const peers = Math.max(active - 1, 0);
const denominator = peers * params.k + proverShare;
return denominator <= 0 ? 0 : proverShare / denominator;
}
function recoveryRows(params, missedEpochs) {
const missed = Math.max(Math.round(missedEpochs), 0);
const rows = [];
for (let i = 1; i <= missed; i += 1) {
rows.push({event: "missed_epoch", timeline_epoch: i, proof_index_after_return: null, score: null, share: 0, relative_weight: 0, at_full_weight: false, raw_weight_loss_eq: 1});
}
const firstElapsed = missed === 0 ? 1 : missed + 1;
let score = successfulUpdate(params.max_score, firstElapsed, params);
for (let proofIdx = 1; proofIdx <= 50000; proofIdx += 1) {
const share = shareFromScore(score, params);
const atFull = score >= params.max_score - 1e-9;
rows.push({
event: "recovery_proof",
timeline_epoch: missed + proofIdx,
proof_index_after_return: proofIdx,
score,
share,
relative_weight: share / params.k,
at_full_weight: atFull,
raw_weight_loss_eq: Math.max(0, 1 - share / params.k)
});
if (atFull) break;
score = successfulUpdate(score, 1, params);
}
return rows;
}
function addEconomics(rows, params, econ) {
const gasUsd = econ.gas_units_per_proof * econ.gas_price_gwei * 1e-9 * econ.eth_usd;
const cashUsd = gasUsd + econ.infra_cost_per_proof_usd;
const poolUsd = econ.epoch_reward_pool_aztc * econ.aztc_usd;
const healthyRewardUsd = poolUsd / Math.max(Math.round(econ.active_full_weight_provers), 1);
let cumulativeCash = 0;
let cumulativeLoss = 0;
let cumulativeRawLoss = 0;
return rows.map(row => {
let rewardUsd = 0;
let cashSpend = 0;
let profitUsd = 0;
let lossUsd = healthyRewardUsd;
if (row.event === "recovery_proof") {
rewardUsd = poolUsd * rewardFraction(row.share, params, econ.active_full_weight_provers);
cashSpend = cashUsd;
profitUsd = rewardUsd - cashSpend;
lossUsd = Math.max(0, healthyRewardUsd - rewardUsd);
}
cumulativeCash += cashSpend;
cumulativeLoss += lossUsd;
cumulativeRawLoss += row.raw_weight_loss_eq;
return {...row, reward_usd: rewardUsd, cash_spend_usd: cashSpend, profit_usd: profitUsd, reward_opportunity_loss_usd: lossUsd, cumulative_cash_spend_usd: cumulativeCash, cumulative_reward_opportunity_loss_usd: cumulativeLoss, cumulative_raw_weight_loss_eq: cumulativeRawLoss};
});
}
function breakEvenShare(params, cashUsd, econ) {
const poolUsd = econ.epoch_reward_pool_aztc * econ.aztc_usd;
if (poolUsd <= 0) return {share: Infinity, relative: Infinity, status: "No rewards: AZTC price or reward pool is zero."};
const active = Math.max(Math.round(econ.active_full_weight_provers), 1);
const peers = Math.max(active - 1, 0);
const requiredFraction = cashUsd / poolUsd;
const maxFraction = 1 / active;
if (requiredFraction <= 0) return {share: 0, relative: 0, status: "Profitable at any positive reward share."};
if (requiredFraction > maxFraction + 1e-15) return {share: Infinity, relative: Infinity, status: "Not profitable even at full weight."};
if (peers === 0) return {share: 0, relative: 0, status: "Single-prover case: any positive share receives the whole pool."};
const share = (requiredFraction * peers * params.k) / (1 - requiredFraction);
return {share, relative: share / params.k, status: "Break-even share found."};
}
function scoreForShare(targetShare, params) {
if (!finite(targetShare)) return Infinity;
const target = Math.max(targetShare, params.minimum);
if (target <= params.minimum) return 0;
if (target >= params.k) return params.max_score;
const delta = Math.sqrt(Math.max(0, (params.k - target) * 1e10 / params.a));
return Math.max(0, params.max_score - delta);
}
function breakEvenAztc(cashUsd, econ) {
if (econ.epoch_reward_pool_aztc <= 0) return Infinity;
return cashUsd * Math.max(Math.round(econ.active_full_weight_provers), 1) / econ.epoch_reward_pool_aztc;
}
function breakEvenGas(econ) {
const reward = econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1);
const remaining = reward - econ.infra_cost_per_proof_usd;
if (remaining < 0 || econ.eth_usd <= 0 || econ.gas_units_per_proof <= 0) return -Infinity;
return remaining / (econ.gas_units_per_proof * 1e-9 * econ.eth_usd);
}
function summarize(params, missed, econ) {
const rows = addEconomics(recoveryRows(params, missed), params, econ);
const proofs = rows.filter(row => row.event === "recovery_proof");
const firstProof = proofs[0];
const cashPerProof = cashCost(econ.gas_units_per_proof, econ.gas_price_gwei, econ.eth_usd, econ.infra_cost_per_proof_usd);
const be = breakEvenShare(params, cashPerProof, econ);
const recoveryProofs = proofs.length;
return {
parameter_set: params.name,
first_return_share_pct: 100 * firstProof.share / params.k,
underweight_recovery_proofs: proofs.filter(row => row.share < params.k - 1e-9).length,
recovery_proofs_to_full_weight: recoveryProofs,
recovery_time_hours: recoveryProofs * DEFAULTS.epoch_minutes / 60,
raw_reward_weight_loss_eq: rows.reduce((sum, row) => sum + row.raw_weight_loss_eq, 0),
reward_opportunity_loss_usd: rows.reduce((sum, row) => sum + row.reward_opportunity_loss_usd, 0),
gross_recovery_cash_spend_usd: proofs.reduce((sum, row) => sum + row.cash_spend_usd, 0),
cash_cost_per_proof_usd: cashPerProof,
full_weight_reward_per_proof_usd: econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1),
full_weight_profit_per_proof_usd: econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1) - cashPerProof,
break_even_relative_weight_pct: finite(be.relative) ? 100 * be.relative : Infinity,
break_even_score: scoreForShare(be.share, params),
break_even_aztc_usd_at_full_weight: breakEvenAztc(cashPerProof, econ),
break_even_gas_gwei_at_full_weight: breakEvenGas(econ),
break_even_status: be.status
};
}
function trace(name, x, y, xaxis = "x", yaxis = "y", color, mode = "lines+markers", dash = null) {
const line = {width: 2};
const marker = {size: 7};
if (color) { line.color = color; marker.color = color; }
if (dash) line.dash = dash;
return {type: "scatter", mode, name, x, y, xaxis, yaxis, line, marker, showlegend: false};
}
function lineTrace(name, x, y, xaxis = "x", yaxis = "y", color = "#6b7280", dash = "dash") {
return {type: "scatter", mode: "lines", name, x, y, xaxis, yaxis, line: {color, dash, width: 2}, showlegend: false};
}
function legendItem(name, color, dash = null) {
return {name, color, dash};
}
function renderLegend(target, items) {
target.innerHTML = items.map(item => `<span class="aztec-legend-item"><span class="aztec-legend-line ${item.dash || ""}" style="--legend-color: ${escapeHtml(item.color)}"></span><span>${escapeHtml(item.name)}</span></span>`).join("");
}
function basePanelLayout(xTitle, yTitle, extra = {}) {
return {
autosize: true,
height: 300,
margin: {l: 58, r: 18, t: 10, b: 50},
showlegend: false,
hovermode: "closest",
xaxis: {title: {text: xTitle}, automargin: true},
yaxis: {title: {text: yTitle}, automargin: true},
...extra
};
}
function panelTraces(traces, xaxis, yaxis) {
return traces.filter(trace => trace.xaxis === xaxis && trace.yaxis === yaxis).map(trace => ({...trace, xaxis: "x", yaxis: "y"}));
}
function legendFromTraces(traces) {
return traces.map(trace => legendItem(trace.name, trace.line?.color || trace.marker?.color || "#6b7280", trace.line?.dash || null));
}
function chartPanel(plotId, legendId, traces, xaxis, yaxis, xTitle, yTitle, extra = {}) {
const selected = panelTraces(traces, xaxis, yaxis);
return {plotId, legendId, traces: selected, legend: legendFromTraces(selected), layout: basePanelLayout(xTitle, yTitle, extra)};
}
function renderPanels(panels, config) {
panels.forEach(panel => {
Plotly.react(byId(panel.plotId), panel.traces, panel.layout, config);
renderLegend(byId(panel.legendId), panel.legend);
});
}
function subplotTitle(text, xDomain, y) {
return {
text,
x: (xDomain[0] + xDomain[1]) / 2,
y,
xref: "paper",
yref: "paper",
xanchor: "center",
yanchor: "bottom",
align: "center",
showarrow: false,
font: {size: 13}
};
}
function setPlotStatus(status, message) {
if (!status) return;
status.textContent = message;
status.hidden = message === "";
}
function waitForPlotly(status, render, attempt = 0) {
if (window.Plotly) { setPlotStatus(status, ""); return true; }
setPlotStatus(status, "Loading Plotly.js...");
if (attempt >= 200) {
setPlotStatus(status, "Plotly.js did not load. Check network access to cdn.plot.ly.");
return false;
}
window.setTimeout(() => {
if (window.Plotly) { setPlotStatus(status, ""); render(); }
else waitForPlotly(status, render, attempt + 1);
}, 50);
return false;
}
function econFrom(prefix) {
return {
active_full_weight_provers: Math.max(1, Math.round(num(`${prefix}-active-provers`))),
epoch_reward_pool_aztc: Math.max(0, num(`${prefix}-reward-pool`)),
aztc_usd: Math.max(0, num(`${prefix}-aztc-usd`)),
eth_usd: Math.max(0, num(`${prefix}-eth-usd`)),
gas_price_gwei: Math.max(0, num(`${prefix}-gas-gwei`)),
gas_units_per_proof: Math.max(0, num(`${prefix}-gas-units`)),
infra_cost_per_proof_usd: Math.max(0, num(`${prefix}-infra-cost`))
};
}
function setDefaultEconomics(prefix) {
setVal(`${prefix}-active-provers`, DEFAULTS.active_full_weight_provers);
setVal(`${prefix}-reward-pool`, DEFAULTS.epoch_reward_pool_aztc);
setVal(`${prefix}-aztc-usd`, DEFAULTS.aztc_usd);
setVal(`${prefix}-eth-usd`, DEFAULTS.eth_usd);
setVal(`${prefix}-gas-gwei`, DEFAULTS.gas_price_gwei);
setVal(`${prefix}-gas-units`, DEFAULTS.gas_units_per_proof);
setVal(`${prefix}-infra-cost`, DEFAULTS.infra_cost_per_proof_usd);
}
function comparisonSummaryRows(missed, econ) {
return Object.values(PRESETS).map(params => {
const row = summarize(params, missed, econ);
return {
"Parameter set": row.parameter_set,
"First return": fmtNumber(row.first_return_share_pct, 2, "", "%"),
"Recovery proofs": fmtInt(row.recovery_proofs_to_full_weight),
"Recovery time": fmtNumber(row.recovery_time_hours, 2, "", " h"),
"Reward loss": fmtNumber(row.reward_opportunity_loss_usd, 2, "$"),
"Gross cash": fmtNumber(row.gross_recovery_cash_spend_usd, 2, "$"),
"Break-even": row.break_even_status
};
});
}
function buildComparisonState(missed, econ) {
const traces = [];
let recoveryMax = 1;
for (const params of Object.values(PRESETS)) {
const rows = addEconomics(recoveryRows(params, missed), params, econ);
const proofs = rows.filter(row => row.event === "recovery_proof");
recoveryMax = Math.max(recoveryMax, ...proofs.map(row => row.proof_index_after_return));
traces.push(trace(params.name, proofs.map(row => row.proof_index_after_return), proofs.map(row => 100 * row.relative_weight), "x", "y", COLORS[params.name]));
traces.push(trace(params.name, proofs.map(row => row.proof_index_after_return), proofs.map(row => row.cumulative_cash_spend_usd), "x2", "y2", COLORS[params.name], "lines+markers", null, true, "legend2"));
}
traces.push(lineTrace("full weight", [1, recoveryMax], [100, 100], "x", "y"));
const missRange = intRange(1, Math.max(10, missed));
for (const params of Object.values(PRESETS)) {
const firstReturnShares = missRange.map(m => {
const firstProof = recoveryRows(params, m).find(row => row.event === "recovery_proof");
return 100 * firstProof.share / params.k;
});
traces.push(trace(params.name, missRange, firstReturnShares, "x3", "y3", COLORS[params.name], "lines+markers", null, true, "legend3"));
}
const gasGrid = linspace(0, Math.max(1, econ.gas_price_gwei * 3), 100);
const fullReward = econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1);
const profits = gasGrid.map(g => fullReward - cashCost(econ.gas_units_per_proof, g, econ.eth_usd, econ.infra_cost_per_proof_usd));
const profitMin = Math.min(...profits);
const profitMax = Math.max(...profits);
const profitPad = Math.max(1, 0.05 * (profitMax - profitMin));
traces.push(trace("full-weight profit", gasGrid, profits, "x4", "y4", "#1f77b4", "lines", null, true, "legend4"));
traces.push(lineTrace("break-even", [gasGrid[0], gasGrid[gasGrid.length - 1]], [0, 0], "x4", "y4", "#6b7280", "dash", true, "legend4"));
traces.push(lineTrace("configured gas price", [econ.gas_price_gwei, econ.gas_price_gwei], [profitMin - profitPad, profitMax + profitPad], "x4", "y4", "#ef4444", "dot", true, "legend4"));
return {
title: `Recovery and cost summary after ${missed} missed epoch(s)`,
panels: [
chartPanel("aztec-comparison-recovery-plot", "aztec-comparison-recovery-legend", traces, "x", "y", "Successful proofs after return", "Relative reward weight (%)"),
chartPanel("aztec-comparison-cash-plot", "aztec-comparison-cash-legend", traces, "x2", "y2", "Successful proofs after return", "Cumulative gross recovery cash spend (USD)"),
chartPanel("aztec-comparison-penalty-plot", "aztec-comparison-penalty-legend", traces, "x3", "y3", "Consecutive missed epochs before returning", "Share on first return proof (%)"),
chartPanel("aztec-comparison-profit-plot", "aztec-comparison-profit-legend", traces, "x4", "y4", "Gas price (gwei)", "Full-weight profit per proof (USD)", {yaxis: {title: {text: "Full-weight profit per proof (USD)"}, automargin: true, range: [profitMin - profitPad, profitMax + profitPad]}})
]
};
}
function renderComparison() {
setDefaultEconomics("aztec-comparison");
const slider = byId("aztec-comparison-missed");
const sliderValue = byId("aztec-comparison-missed-value");
const table = byId("aztec-comparison-summary");
const status = byId("aztec-comparison-plot-status");
const title = byId("aztec-comparison-plot-title");
const config = {responsive: true, displayModeBar: true};
function render() {
const missed = Math.round(num("aztec-comparison-missed"));
const econ = econFrom("aztec-comparison");
sliderValue.value = missed;
renderTable(table, comparisonColumns, comparisonSummaryRows(missed, econ));
if (!waitForPlotly(status, render)) return;
const state = buildComparisonState(missed, econ);
title.textContent = state.title;
renderPanels(state.panels, config);
}
byId("aztec-comparison-widget").querySelectorAll("input").forEach(input => input.addEventListener("input", render));
render();
}
function pathRowsForTable(rows) {
return rows.slice(0, 50).map(row => ({
event: row.event,
timeline_epoch: fmtInt(row.timeline_epoch),
proof_index_after_return: row.proof_index_after_return === null ? "" : fmtInt(row.proof_index_after_return),
score: row.score === null ? "" : fmtInt(row.score),
relative_weight: fmtNumber(100 * row.relative_weight, 2, "", "%"),
reward_usd: fmtNumber(row.reward_usd, 2, "$"),
cash_spend_usd: fmtNumber(row.cash_spend_usd, 2, "$"),
profit_usd: fmtNumber(row.profit_usd, 2, "$"),
reward_opportunity_loss_usd: fmtNumber(row.reward_opportunity_loss_usd, 2, "$"),
cumulative_cash_spend_usd: fmtNumber(row.cumulative_cash_spend_usd, 2, "$"),
cumulative_reward_opportunity_loss_usd: fmtNumber(row.cumulative_reward_opportunity_loss_usd, 2, "$"),
}));
}
function singleSummaryRows(summary) {
return [
{Metric: "Recovery proofs to full weight", Value: fmtInt(summary.recovery_proofs_to_full_weight)},
{Metric: "Underweight recovery proofs", Value: fmtInt(summary.underweight_recovery_proofs)},
{Metric: "Recovery time", Value: fmtNumber(summary.recovery_time_hours, 2, "", " h")},
{Metric: "First-return weight", Value: fmtNumber(summary.first_return_share_pct, 2, "", "%")},
{Metric: "Gross recovery cash spend", Value: fmtNumber(summary.gross_recovery_cash_spend_usd, 2, "$" )},
{Metric: "Reward opportunity loss", Value: fmtNumber(summary.reward_opportunity_loss_usd, 2, "$" )},
{Metric: "Full-weight reward/proof", Value: fmtNumber(summary.full_weight_reward_per_proof_usd, 2, "$" )},
{Metric: "Cash cost/proof", Value: fmtNumber(summary.cash_cost_per_proof_usd, 2, "$" )},
{Metric: "Break-even status", Value: summary.break_even_status}
];
}
function customParams() {
return {
name: "Custom",
increment: num("aztec-custom-increment"),
max_score: num("aztec-custom-max-score"),
a: num("aztec-custom-a"),
minimum: num("aztec-custom-minimum"),
k: num("aztec-custom-k"),
decay_rate: num("aztec-custom-decay-rate")
};
}
function paramsForSingle() {
const selected = byId("aztec-single-preset").value;
return selected === "Custom" ? customParams() : PRESETS[selected];
}
function buildSingleState(params, missed, econ) {
const rows = addEconomics(recoveryRows(params, missed), params, econ);
const proofs = rows.filter(row => row.event === "recovery_proof");
const summary = summarize(params, missed, econ);
const cashPerProof = summary.cash_cost_per_proof_usd;
const be = breakEvenShare(params, cashPerProof, econ);
const requiredScore = scoreForShare(be.share, params);
const scoreGrid = linspace(0, params.max_score, 500);
const missRange = intRange(1, 20);
const firstReturnRows = missRange.map(m => {
const firstRows = addEconomics(recoveryRows(params, m), params, econ);
return firstRows.find(row => row.event === "recovery_proof");
});
const proofX = proofs.map(row => row.proof_index_after_return);
const proofMin = Math.min(...proofX);
const proofMax = Math.max(...proofX);
const costX = rows.map(row => row.timeline_epoch);
const rewardProfitValues = proofs.flatMap(row => [row.reward_usd, row.profit_usd]).concat([cashPerProof, 0]);
const rewardProfitMin = Math.min(...rewardProfitValues);
const rewardProfitMax = Math.max(...rewardProfitValues);
const rewardProfitPad = Math.max(1, 0.05 * (rewardProfitMax - rewardProfitMin));
const firstProfit = firstReturnRows.map(row => row.profit_usd);
const firstProfitMin = Math.min(...firstProfit, 0);
const firstProfitMax = Math.max(...firstProfit, 0);
const firstProfitPad = Math.max(1, 0.05 * (firstProfitMax - firstProfitMin));
const traces = [
trace("share curve", scoreGrid.map(score => score / params.max_score), scoreGrid.map(score => 100 * shareFromScore(score, params) / params.k), "x", "y", "#1f77b4", "lines"),
trace("recovery proofs", proofs.map(row => row.score / params.max_score), proofs.map(row => 100 * row.relative_weight), "x", "y", "#ff7f0e", "markers"),
trace("relative weight", proofX, proofs.map(row => 100 * row.relative_weight), "x2", "y2", "#1f77b4", "lines+markers", null, true, "legend2"),
lineTrace("full weight", [proofMin, proofMax], [100, 100], "x2", "y2", "#6b7280", "dash", true, "legend2"),
trace("reward per proof", proofX, proofs.map(row => row.reward_usd), "x3", "y3", "#1f77b4", "lines+markers", null, true, "legend3"),
lineTrace("cash cost per proof", [proofMin, proofMax], [cashPerProof, cashPerProof], "x3", "y3", "#ef4444", "dash", true, "legend3"),
trace("profit per proof", proofX, proofs.map(row => row.profit_usd), "x3", "y3", "#2ca02c", "lines+markers", null, true, "legend3"),
lineTrace("break-even profit", [proofMin, proofMax], [0, 0], "x3", "y3", "#6b7280", "dash", true, "legend3"),
trace("cumulative gross cash spend", rows.map(row => row.timeline_epoch), rows.map(row => row.cumulative_cash_spend_usd), "x4", "y4", "#1f77b4", "lines+markers", null, true, "legend4"),
trace("cumulative reward opportunity loss", rows.map(row => row.timeline_epoch), rows.map(row => row.cumulative_reward_opportunity_loss_usd), "x4", "y4", "#ff7f0e", "lines+markers", null, true, "legend4"),
trace("first-return weight", missRange, firstReturnRows.map(row => 100 * row.relative_weight), "x5", "y5", "#1f77b4", "lines+markers", null, true, "legend5"),
trace("first-return profit", missRange, firstProfit, "x6", "y6", "#1f77b4", "lines+markers", null, true, "legend6"),
lineTrace("break-even profit", [1, 20], [0, 0], "x6", "y6", "#6b7280", "dash", true, "legend6")
];
if (finite(be.relative)) {
traces.push(lineTrace("break-even relative weight", [0, 1], [100 * be.relative, 100 * be.relative], "x", "y"));
traces.push(lineTrace("break-even relative weight", [proofMin, proofMax], [100 * be.relative, 100 * be.relative], "x2", "y2", "#6b7280", "dot", true, "legend2"));
traces.push(lineTrace("break-even relative weight", [1, 20], [100 * be.relative, 100 * be.relative], "x5", "y5", "#6b7280", "dash", true, "legend5"));
if (finite(requiredScore)) traces.push(lineTrace("break-even score", [requiredScore / params.max_score, requiredScore / params.max_score], [0, 105], "x", "y", "#ef4444", "dot"));
}
return {
summary,
rows,
headline: `After ${missed} missed epoch(s), ${params.name} needs ${fmtInt(proofs.length)} successful proof(s) to reach the first full-weight proof. Gross recovery cash spend: ${fmtNumber(summary.gross_recovery_cash_spend_usd, 2, "$")}. Reward opportunity loss: ${fmtNumber(summary.reward_opportunity_loss_usd, 2, "$")}. Full-weight reward/proof: ${fmtNumber(summary.full_weight_reward_per_proof_usd, 2, "$")}. Cash cost/proof: ${fmtNumber(summary.cash_cost_per_proof_usd, 2, "$")}.`,
title: `${params.name}: recovery panels after ${missed} missed epoch(s)`,
panels: [
chartPanel("aztec-single-reward-plot", "aztec-single-reward-legend", traces, "x", "y", "Score / maxScore", "Relative weight (%)", {yaxis: {title: {text: "Relative weight (%)"}, automargin: true, range: [0, 105]}}),
chartPanel("aztec-single-recovery-plot", "aztec-single-recovery-legend", traces, "x2", "y2", "Successful proofs after return", "Relative weight (%)"),
chartPanel("aztec-single-revenue-plot", "aztec-single-revenue-legend", traces, "x3", "y3", "Successful proofs after return", "USD", {yaxis: {title: {text: "USD"}, automargin: true, range: [rewardProfitMin - rewardProfitPad, rewardProfitMax + rewardProfitPad]}}),
chartPanel("aztec-single-cost-plot", "aztec-single-cost-legend", traces, "x4", "y4", "Timeline epoch after last healthy proof", "USD", {xaxis: {title: {text: "Timeline epoch after last healthy proof"}, automargin: true, range: [Math.min(...costX), Math.max(...costX)]}}),
chartPanel("aztec-single-first-weight-plot", "aztec-single-first-weight-legend", traces, "x5", "y5", "Consecutive missed epochs", "First-return relative weight (%)"),
chartPanel("aztec-single-first-profit-plot", "aztec-single-first-profit-legend", traces, "x6", "y6", "Consecutive missed epochs", "First-return proof profit (USD)", {yaxis: {title: {text: "First-return proof profit (USD)"}, automargin: true, range: [firstProfitMin - firstProfitPad, firstProfitMax + firstProfitPad]}})
]
};
}
function setDefaultCustomParams() {
const params = PRESETS["AZIP-5 proposed"];
setVal("aztec-custom-increment", params.increment);
setVal("aztec-custom-max-score", params.max_score);
setVal("aztec-custom-a", params.a);
setVal("aztec-custom-minimum", params.minimum);
setVal("aztec-custom-k", params.k);
setVal("aztec-custom-decay-rate", params.decay_rate);
}
function renderSingle() {
setDefaultEconomics("aztec-single");
setDefaultCustomParams();
const presetSelect = byId("aztec-single-preset");
presetSelect.innerHTML = Object.keys(PRESETS).concat(["Custom"]).map(name => `<option value="${name}">${escapeHtml(name)}</option>`).join("");
presetSelect.value = "AZIP-5 proposed";
const config = {responsive: true, displayModeBar: true};
function render() {
const missed = Math.round(num("aztec-single-missed"));
const econ = econFrom("aztec-single");
const params = paramsForSingle();
byId("aztec-single-missed-value").value = missed;
byId("aztec-single-custom").disabled = presetSelect.value !== "Custom";
const state = buildSingleState(params, missed, econ);
byId("aztec-single-headline").textContent = state.headline;
renderTable(byId("aztec-single-summary"), summaryColumns, singleSummaryRows(state.summary));
renderTable(byId("aztec-single-path"), pathColumns, pathRowsForTable(state.rows));
byId("aztec-single-path-note").textContent = state.rows.length > 50 ? `Showing first 50 of ${fmtInt(state.rows.length)} rows.` : "";
const status = byId("aztec-single-plot-status");
if (!waitForPlotly(status, render)) return;
byId("aztec-single-plot-title").textContent = state.title;
renderPanels(state.panels, config);
}
byId("aztec-single-widget").querySelectorAll("input, select").forEach(input => input.addEventListener("input", render));
presetSelect.addEventListener("change", render);
render();
}
window.aztecRewardSim = {renderComparison, renderSingle};
renderComparison();
})();
</script>
"""
html = html.replace("__PLOTLY_CDN__", PLOTLY_CDN_URL)
html = html.replace("__PRESETS__", _script_safe_json(PLOTLY_PRESETS))
html = html.replace("__DEFAULTS__", _script_safe_json(PLOTLY_DEFAULTS))
display(HTML(html))
render_preset_comparison_plotly_controls()