Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions prime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,33 @@ the key does not apply to), `class_budget_replies` the coinbasers held to it, an
appears in the stats of a Prime that has never had the key; one that had it and no longer does
keeps reporting `carry_reserved` while the blocks it recorded are in memory.

### Template budgets

A gateway on a current CONVOY build (v0.5.0beta) fits every size class to its template
(`datum_stratum_coinbase_fit_to_template`). While the mempool fills blocks to the chain's
800 000 weight units, all a template leaves is the node's coinbase reserve, about 2 000 bytes,
and every class keeps the first 50 or so payees of a list of over 100. Blocks 977088, 977570,
977906, 977949 and 977993 were each Partial that way, and in the week to 2026-10-19 so were 7 of
33 blocks from v0.5.0beta gateways and 65 of 179 from v0.4.1-beta ones. Each owes the rest
through a make-good that waits 6 480 blocks, and the fee wallet's offender rule charged the
gateway's own address the custody fee for a block that was only full.

`template-budget = true` holds a session's coinbasers to the payee bytes the fullest template its
shares cited in the last minute leaves every class: `(template-weight-limit - txn_total_weight -
712) / 4`, less the class's fixed part (124 + 85 + 10 bytes and the pool's script), the list's
own pool output and a 128-byte margin. On the five blocks above that is 189 to 208 bytes under
what CONVOY kept, so a list held to it is kept whole. Where it is under the pool's own budget it
is the reply's budget, or the smaller of it and a learned class budget; otherwise nothing
changes. Whoever does not fit is `OverBudget` and waits in carry, largest balances placed first,
exactly as under a class budget.

It is a class budget in every other respect: the same sessions (CONVOY-generation, not the house
gateway, lazarus-gateway, lazarus-split, ratum or a held-split build), the carry ceiling,
`carry_reserved_sats` on capped blocks, the `class_budget_replies` counters, and it needs
`class-budget-fee-wallet-reserves = true` (the live fee wallet holds `carry_reserved_sats` back).
`template-weight-limit` (default 800 000) is the `weightlimit` the gateways' nodes report; the
class-budget check that a template left room uses it too, and the 164-byte BLAKE2b header.

### Restarts used to throw away a burst of good work

A reconnecting gateway is still serving jobs whose coinbases carry the split from coinbaser ids
Expand Down
65 changes: 65 additions & 0 deletions prime/primed/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ pub struct Config {
/// meanwhile are Partial and owe as they do without `class-budget`. Default 5 XBT.
#[serde(default = "d_class_budget_carry_ceiling")]
pub class_budget_carry_ceiling: u64,
/// Hold each coinbaser to what the gateway's recent templates leave room for. CONVOY fits
/// every size class to the template (`datum_stratum_coinbase_fit_to_template`): when the
/// mempool fills a block to `template-weight-limit`, only the node's coinbase reserve is left,
/// about 2 000 bytes, and the class keeps the first 50 or so of a list of over 100. Every such
/// block was Partial: 7 of 33 from v0.5.0beta gateways and 65 of 179 from v0.4.1-beta in the
/// week to 2026-10-19, each owing the rest through a make-good that waits 6 480 blocks. Held to
/// that room, the list fits, the block is a full split, and whoever does not fit waits in
/// carry for a later coinbase instead. Same sessions, guards and accounting as `class-budget`
/// (`carry_reserved_sats`, the carry ceiling); ignored, and said so at startup, unless
/// `class-budget-fee-wallet-reserves` is set too. Off (default): replies are as they were.
#[serde(default)]
pub template_budget: bool,
/// The block weight a gateway's template is filled to: the `weightlimit` its node's
/// `getblocktemplate` reports (800 000 on this chain).
#[serde(default = "d_template_weight_limit")]
pub template_weight_limit: u32,
/// Test hook: sleep this long before answering a coinbaser, so a stock gateway's
/// 5 s fetch times out. 0 (default) is production.
#[serde(default)]
Expand Down Expand Up @@ -254,6 +270,9 @@ pub struct Config {
/// `class-budget = true` without `class-budget-fee-wallet-reserves`, turned off by `load`.
#[serde(skip)]
class_budget_unreserved: bool,
/// The same for `template-budget = true`.
#[serde(skip)]
template_budget_unreserved: bool,
}

fn d_listen() -> SocketAddr {
Expand Down Expand Up @@ -344,6 +363,10 @@ fn d_solo_tag() -> String {
/// simulation of a 17-output cap put the added float at: a guard at the level it was expected
/// to settle, so that a float that keeps growing (CONVOY hashrate leaving, the pool shrinking)
/// stops growing there, not one that trims the expected case.
fn d_template_weight_limit() -> u32 {
800_000
}

fn d_class_budget_carry_ceiling() -> u64 {
500_000_000
}
Expand Down Expand Up @@ -472,6 +495,13 @@ impl Config {
c.class_budget = false;
c.class_budget_unreserved = true;
}
if c.template_budget && !c.class_budget_fee_wallet_reserves {
c.template_budget = false;
c.template_budget_unreserved = true;
}
if c.template_weight_limit < 100_000 {
return Err(format!("template-weight-limit {} is not a block's weight", c.template_weight_limit));
}
if c.key_file.is_none() {
// A data dir left by lazarus-prime keeps its identity: same key file, same pubkey.
let ours = c.data_dir.join("prime.key");
Expand Down Expand Up @@ -513,6 +543,14 @@ impl Config {
.into(),
);
}
if self.template_budget_unreserved {
v.push(
"template-budget = true is ignored until class-budget-fee-wallet-reserves = true says the fee wallet \
holds back carry_reserved_sats: a template-capped block is a split owing nothing whose pool output \
holds the earnings its budget deferred to carry (README, \"Class budgets\")"
.into(),
);
}
if self.activation_height.is_some() {
v.push("activation-height is ignored: every share is verified as BLAKE2b header v2; SHA256d shares are rejected as bad-version".into());
}
Expand Down Expand Up @@ -792,4 +830,31 @@ require-split-gateway = true
assert!(c.class_budget && notes(&c).is_empty());
std::fs::remove_dir_all(&dir).unwrap();
}

/// `template-budget` takes the same word from the operator as `class-budget`, and alone is
/// off; the weight limit defaults to this chain's.
#[test]
fn template_budget_is_off_until_the_fee_wallet_is_said_to_hold_back_its_carry() {
let dir = std::env::temp_dir().join(format!("primed-cfg-tb-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("prime.toml");
let base = LEGACY.replace("/home/umbrel/blake2b/lazarus-prime", dir.to_str().unwrap());
let load = |extra: &str| {
std::fs::write(&p, format!("{base}{extra}")).unwrap();
Config::load(&p)
};
let notes = |c: &Config| -> Vec<String> {
c.legacy_notes().into_iter().filter(|n| n.contains("template-budget")).collect()
};
let c = load("").unwrap();
assert!(!c.template_budget && c.template_weight_limit == 800_000 && notes(&c).is_empty());
let c = load("template-budget = true\n").unwrap();
assert!(!c.template_budget && notes(&c).len() == 1);
let c = load("template-budget = true\nclass-budget-fee-wallet-reserves = true\n").unwrap();
assert!(c.template_budget && !c.class_budget && notes(&c).is_empty());
let c = load("template-weight-limit = 4000000\n").unwrap();
assert_eq!(c.template_weight_limit, 4_000_000);
assert!(load("template-weight-limit = 4000\n").is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
}
Loading
Loading