iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 9e17dfe4cff42c7b105e6b8cac8e60bcbd6ddd57
parent 0b6c5a352ee3c09593a5a1fdb86e0081a34be465
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 19 Aug 2026 13:44:35 +0200

Add Stratum endpoint settings

Diffstat:
Msrc/adapters/config_store.rs | 25+++++++++++++++++++++++++
Msrc/adapters/http.rs | 3++-
Msrc/adapters/http/actions.rs | 30++++++++++++++++++++++++++++--
Msrc/adapters/http/api.rs | 2++
Msrc/adapters/http/index_html.rs | 25++++++++++++++++++++++---
Msrc/adapters/http/types.rs | 8++++++++
Msrc/cli.rs | 30++++++++++++++++++++++++++++++
Msrc/main.rs | 12++++++++----
Msrc/main_tests.rs | 43++++++++++++++++++++++++++++++++++++++++++-
Mwww/assets/iuna-ui.js | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 240 insertions(+), 11 deletions(-)

diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs @@ -21,6 +21,7 @@ pub const DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT: u8 = 50; pub const DEFAULT_POW_MINING_WORKERS: u8 = 1; pub const MAX_POW_MINING_WORKERS: u8 = 32; pub const DEFAULT_P2P_BIND_PORT: u16 = 9444; +pub const DEFAULT_STRATUM_BIND_PORT: u16 = 3333; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct UiConfig { @@ -37,6 +38,8 @@ pub struct UiConfig { pub p2p_accept_inbound: bool, pub p2p_bind_port: u16, pub p2p_announce_addr: Option<String>, + pub stratum_enabled: bool, + pub stratum_bind_port: u16, pub peers: Vec<String>, pub address_book: BTreeMap<String, String>, } @@ -56,6 +59,8 @@ impl Default for UiConfig { p2p_accept_inbound: false, p2p_bind_port: DEFAULT_P2P_BIND_PORT, p2p_announce_addr: None, + stratum_enabled: false, + stratum_bind_port: DEFAULT_STRATUM_BIND_PORT, peers: Vec::new(), address_book: BTreeMap::new(), } @@ -91,6 +96,10 @@ struct ConfigFile { #[serde(default)] p2p_announce_addr: Option<String>, #[serde(default)] + stratum_enabled: bool, + #[serde(default = "default_stratum_bind_port")] + stratum_bind_port: u16, + #[serde(default)] peers: Vec<String>, #[serde(default)] address_book: BTreeMap<String, String>, @@ -127,6 +136,8 @@ pub fn save(path: &Path, config: &UiConfig) -> Result<()> { p2p_accept_inbound: Some(config.p2p_accept_inbound), p2p_bind_port: config.p2p_bind_port, p2p_announce_addr: config.p2p_announce_addr.clone(), + stratum_enabled: config.stratum_enabled, + stratum_bind_port: config.stratum_bind_port, peers: config.peers.clone(), address_book: config.address_book.clone(), }; @@ -185,6 +196,8 @@ fn load(path: &Path) -> Result<UiConfig> { p2p_accept_inbound, p2p_bind_port: stored.p2p_bind_port, p2p_announce_addr: stored.p2p_announce_addr, + stratum_enabled: stored.stratum_enabled, + stratum_bind_port: stored.stratum_bind_port, peers: stored.peers, address_book: stored.address_book, }) @@ -202,6 +215,10 @@ fn default_p2p_bind_port() -> u16 { DEFAULT_P2P_BIND_PORT } +fn default_stratum_bind_port() -> u16 { + DEFAULT_STRATUM_BIND_PORT +} + fn create_config_file(path: &Path) -> Result<File> { let mut options = OpenOptions::new(); options.write(true).create(true).truncate(true); @@ -246,6 +263,8 @@ mod tests { assert!(stored.contains("\"p2p_accept_inbound\": false")); assert!(stored.contains("\"p2p_bind_port\": 9444")); assert!(stored.contains("\"p2p_announce_addr\": null")); + assert!(stored.contains("\"stratum_enabled\": false")); + assert!(stored.contains("\"stratum_bind_port\": 3333")); assert!(stored.contains("\"peers\": []")); assert!(stored.contains("\"address_book\": {}")); } @@ -269,6 +288,8 @@ mod tests { p2p_accept_inbound: true, p2p_bind_port: 9555, p2p_announce_addr: Some("203.0.113.10:9444".to_string()), + stratum_enabled: true, + stratum_bind_port: 3334, peers: vec!["127.0.0.1:9444".to_string()], address_book: [("iuna-address".to_string(), "Alice".to_string())].into(), ..UiConfig::default() @@ -291,6 +312,8 @@ mod tests { config.p2p_announce_addr.as_deref(), Some("203.0.113.10:9444") ); + assert!(config.stratum_enabled); + assert_eq!(config.stratum_bind_port, 3334); assert_eq!(config.peers, vec!["127.0.0.1:9444"]); assert_eq!( config.address_book.get("iuna-address"), @@ -336,6 +359,8 @@ mod tests { assert!(!config.keep_track_of_metrics); assert!(!config.p2p_accept_inbound); assert_eq!(config.p2p_bind_port, 9444); + assert!(!config.stratum_enabled); + assert_eq!(config.stratum_bind_port, 3333); assert_eq!(config.peers, vec!["127.0.0.1:9444"]); assert!(config.address_book.is_empty()); } diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -39,7 +39,7 @@ use actions::{ api_address_book_delete_form, api_address_book_form, api_burn_fee_estimate_form, api_burn_per_block_form, api_chain_reset_form, api_metrics_settings_form, api_mine_fee_estimate_form, api_p2p_announce_form, api_p2p_inbound_form, api_peer_delete_form, - api_peer_form, api_pow_mining_form, api_recovery_vdf_settings_form, + api_peer_form, api_pow_mining_form, api_recovery_vdf_settings_form, api_stratum_settings_form, api_transfer_fee_estimate_form, api_transfer_form, api_wallet_generate_form, api_wallet_import_form, apply_config_form, burn_per_block_form, peer_form, transfer_form, }; @@ -183,6 +183,7 @@ pub async fn serve( .route("/api/settings/chain-reset", post(api_chain_reset_form)) .route("/api/settings/p2p-inbound", post(api_p2p_inbound_form)) .route("/api/settings/p2p-announce", post(api_p2p_announce_form)) + .route("/api/settings/stratum", post(api_stratum_settings_form)) .route("/api/transfer", post(api_transfer_form)) .route("/settings/burn-per-block", post(burn_per_block_form)) .route("/transfer", post(transfer_form)) diff --git a/src/adapters/http/actions.rs b/src/adapters/http/actions.rs @@ -12,8 +12,8 @@ use tokio::sync::Mutex; use super::types::{ ActionResponse, AddressBookDeleteForm, AddressBookForm, BurnSettingsForm, ChainResetForm, ConfigForm, FeeEstimateResponse, MetricsSettingsForm, P2pAnnounceForm, P2pInboundForm, - PeerForm, PowMiningForm, RecoveryVdfSettingsForm, SeedPhraseForm, TransferForm, - WalletSetupResponse, + PeerForm, PowMiningForm, RecoveryVdfSettingsForm, SeedPhraseForm, StratumSettingsForm, + TransferForm, WalletSetupResponse, }; use super::{ HttpState, action_json, api_error, config_store, estimate_burn_fee, estimate_mine_fee, @@ -136,6 +136,13 @@ pub(super) async fn api_p2p_inbound_form( action_json(set_p2p_accept_inbound(&state, form.enabled, form.bind_port).await) } +pub(super) async fn api_stratum_settings_form( + State(state): State<HttpState>, + Form(form): Form<StratumSettingsForm>, +) -> Json<ActionResponse> { + action_json(set_stratum_settings(&state, form.enabled, form.bind_port).await) +} + pub(super) async fn burn_per_block_form( State(state): State<HttpState>, Form(form): Form<BurnSettingsForm>, @@ -394,6 +401,25 @@ pub(super) async fn set_p2p_accept_inbound( Ok(()) } +pub(super) async fn set_stratum_settings( + state: &HttpState, + enabled: bool, + bind_port: Option<u16>, +) -> Result<()> { + let bind_port = bind_port.unwrap_or(config_store::DEFAULT_STRATUM_BIND_PORT); + if bind_port == 0 { + bail!("Stratum bind port must be between 1 and 65535"); + } + + let mut config = state.ui_config.lock().await; + let mut next_config = config.clone(); + next_config.stratum_enabled = enabled; + next_config.stratum_bind_port = bind_port; + config_store::save(&state.config_path, &next_config)?; + *config = next_config; + Ok(()) +} + async fn replace_metrics_for_snapshot( store: &SqliteUiDataStore, snapshot: crate::domain::ChainSnapshot, diff --git a/src/adapters/http/api.rs b/src/adapters/http/api.rs @@ -76,6 +76,8 @@ pub(super) async fn api_config(State(state): State<HttpState>) -> Json<ConfigRes config: state.ui_config.lock().await.clone(), p2p_inbound_runtime_active: state.gossip.accepts_inbound().await, p2p_runtime_bind_addr: state.gossip.listen_addr().to_string(), + stratum_runtime_enabled: state.stratum.enabled, + stratum_runtime_listen_addr: state.stratum.listen_addr.clone(), }) } diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -761,9 +761,9 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> </div> <div class="fee-preview" x-text="autoPowStatusLabel()"></div> </div> - <div class="panel-separator"></div> - <div class="stratum-config"> - <div class="stratum-note">Start the node with <code>--stratum 0.0.0.0:3333</code> to expose a Stratum V1 endpoint for ASIC miners. Use the pool URL below in the miner configuration.</div> + <div class="panel-separator" x-show="stratumRuntimeEnabled()"></div> + <div class="stratum-config" x-show="stratumRuntimeEnabled()"> + <div class="stratum-note">Use the pool URL below in the miner configuration.</div> <div class="stratum-fields" aria-label="Stratum settings"> <div class="stratum-field"> <div class="stratum-label">Status</div> @@ -1197,6 +1197,25 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> <div class="setup-actions"><button class="primary" type="submit">Save</button></div> </form> </div> + <div class="panel" x-show="advancedMode()"> + <div class="settings-mode-row"> + <div class="settings-mode-copy"> + <div class="settings-mode-title">Stratum endpoint</div> + <div class="muted" x-text="stratumEnabled ? 'ASIC miners can connect after the listener is active.' : 'Stratum listener is disabled.'"></div> + </div> + <label class="toggle-switch" :class="{ active: stratumEnabled }"> + <input type="checkbox" :checked="stratumEnabled" @change="setStratumEnabled($event.target.checked)"> + <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> + <span class="toggle-text" x-text="stratumEnabled ? 'On' : 'Off'"></span> + </label> + </div> + <form class="settings-form" x-show="stratumEnabled" x-transition @submit.prevent="saveStratumSettings"> + <label>Bind port<input x-model.number="stratumBindPort" @input="stratumBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> + <div class="muted" x-text="stratumRestartMessage() || 'Stratum is using the configured runtime setting.'"></div> + <div class="setup-actions"><button class="primary" type="submit">Save</button></div> + </form> + <div class="muted" x-show="!stratumEnabled && stratumRestartRequired()" x-text="stratumRestartMessage()"></div> + </div> <div class="panel"> <h3>Change Password</h3> <div class="setup-feedback" :class="settingsFeedback?.kind" x-show="settingsFeedback" x-transition x-text="settingsFeedback?.message"></div> diff --git a/src/adapters/http/types.rs b/src/adapters/http/types.rs @@ -99,12 +99,20 @@ pub(super) struct P2pInboundForm { pub(super) bind_port: Option<u16>, } +#[derive(Debug, Deserialize)] +pub(super) struct StratumSettingsForm { + pub(super) enabled: bool, + pub(super) bind_port: Option<u16>, +} + #[derive(Debug, Serialize)] pub(super) struct ConfigResponse { #[serde(flatten)] pub(super) config: UiConfig, pub(super) p2p_inbound_runtime_active: bool, pub(super) p2p_runtime_bind_addr: String, + pub(super) stratum_runtime_enabled: bool, + pub(super) stratum_runtime_listen_addr: Option<String>, } #[derive(Debug, Deserialize)] diff --git a/src/cli.rs b/src/cli.rs @@ -36,6 +36,17 @@ pub(crate) fn configured_p2p_bind_addr( opts.p2p_addr } +pub(crate) fn configured_stratum_addr( + opts: &CliOptions, + ui_config: &config_store::UiConfig, +) -> Option<SocketAddr> { + opts.stratum_addr.or_else(|| { + ui_config + .stratum_enabled + .then(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, ui_config.stratum_bind_port))) + }) +} + pub(crate) fn apply_cli_p2p_config_overrides( opts: &CliOptions, ui_config: &mut config_store::UiConfig, @@ -61,6 +72,25 @@ pub(crate) fn apply_cli_p2p_config_overrides( dirty } +pub(crate) fn apply_cli_stratum_config_overrides( + opts: &CliOptions, + ui_config: &mut config_store::UiConfig, +) -> bool { + let Some(addr) = opts.stratum_addr else { + return false; + }; + let mut dirty = false; + if !ui_config.stratum_enabled { + ui_config.stratum_enabled = true; + dirty = true; + } + if ui_config.stratum_bind_port != addr.port() { + ui_config.stratum_bind_port = addr.port(); + dirty = true; + } + dirty +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ChainMode { Setup, diff --git a/src/main.rs b/src/main.rs @@ -25,8 +25,9 @@ use tokio::sync::Mutex; mod cli; use cli::{ - ChainMode, CliOptions, apply_cli_p2p_config_overrides, configured_p2p_announce_addr, - configured_p2p_bind_addr, initial_burn_fee, initial_burn_per_block, validate_wallet_for_mode, + ChainMode, CliOptions, apply_cli_p2p_config_overrides, apply_cli_stratum_config_overrides, + configured_p2p_announce_addr, configured_p2p_bind_addr, configured_stratum_addr, + initial_burn_fee, initial_burn_per_block, validate_wallet_for_mode, }; #[cfg(test)] use cli::{default_data_dir, help_text}; @@ -62,9 +63,12 @@ async fn main() -> Result<()> { ); } let mut ui_config = config_store::load_or_create(&config_path)?; - let ui_config_dirty = apply_cli_p2p_config_overrides(&opts, &mut ui_config); + let p2p_config_dirty = apply_cli_p2p_config_overrides(&opts, &mut ui_config); + let stratum_config_dirty = apply_cli_stratum_config_overrides(&opts, &mut ui_config); + let ui_config_dirty = p2p_config_dirty || stratum_config_dirty; let p2p_announce_addr = configured_p2p_announce_addr(&opts, &ui_config)?; let configured_p2p_addr = configured_p2p_bind_addr(&opts, &ui_config); + let configured_stratum_addr = configured_stratum_addr(&opts, &ui_config); let p2p_accept_inbound = ui_config.p2p_accept_inbound; let advertised_p2p_addr = p2p_announce_addr.unwrap_or(configured_p2p_addr); let wallet_load = load_startup_wallet(&wallet_path)?; @@ -155,7 +159,7 @@ async fn main() -> Result<()> { enabled: false, listen_addr: None, }; - if let Some(stratum_addr) = opts.stratum_addr { + if let Some(stratum_addr) = configured_stratum_addr { let stratum = stratum::StratumServer::start(Arc::clone(&node), gossip.clone(), stratum_addr).await?; println!("stratum listener: {}", stratum.listen_addr()); diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -14,7 +14,8 @@ use tokio::sync::Mutex; use super::{ ChainMode, CliOptions, GENESIS_INITIAL_BURN_FEE, GENESIS_INITIAL_BURN_PER_BLOCK, StartupWallet, - apply_cli_p2p_config_overrides, configured_p2p_announce_addr, configured_p2p_bind_addr, + apply_cli_p2p_config_overrides, apply_cli_stratum_config_overrides, + configured_p2p_announce_addr, configured_p2p_bind_addr, configured_stratum_addr, extrapolate_vdf_rounds, help_text, initial_burn_fee, initial_burn_per_block, initialize_ledger, load_startup_wallet, measure_vdf_rounds, persist_chain_snapshot, project_ui_data_store, run_chain_persistence_with_interval, validate_wallet_for_mode, @@ -77,6 +78,36 @@ fn stratum_port_can_be_configured() { } #[test] +fn configured_stratum_addr_uses_settings_when_enabled() { + let opts = parse(&[]).unwrap().unwrap(); + let config = UiConfig { + stratum_enabled: true, + stratum_bind_port: 3334, + ..UiConfig::default() + }; + + assert_eq!( + configured_stratum_addr(&opts, &config).unwrap().to_string(), + "0.0.0.0:3334" + ); +} + +#[test] +fn configured_stratum_addr_prefers_cli_addr() { + let opts = parse(&["--stratum", "127.0.0.1:3335"]).unwrap().unwrap(); + let config = UiConfig { + stratum_enabled: true, + stratum_bind_port: 3334, + ..UiConfig::default() + }; + + assert_eq!( + configured_stratum_addr(&opts, &config).unwrap().to_string(), + "127.0.0.1:3335" + ); +} + +#[test] fn debug_logging_can_be_enabled() { assert!(!parse(&[]).unwrap().unwrap().debug); assert!(parse(&["--debug"]).unwrap().unwrap().debug); @@ -345,6 +376,16 @@ fn cli_p2p_port_overrides_config_bind_port() { } #[test] +fn cli_stratum_addr_enables_and_persists_config_port() { + let opts = parse(&["--stratum", "127.0.0.1:3335"]).unwrap().unwrap(); + let mut config = UiConfig::default(); + + assert!(apply_cli_stratum_config_overrides(&opts, &mut config)); + assert!(config.stratum_enabled); + assert_eq!(config.stratum_bind_port, 3335); +} + +#[test] fn http_management_port_defaults_to_iuna_port() { let opts = parse(&[]).unwrap().unwrap(); assert_eq!(opts.http_addr.to_string(), "127.0.0.1:18661"); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -67,6 +67,9 @@ window.iunaApp = function iunaApp() { p2pBindPortDirty: false, p2pAnnounceAddr: "", p2pAnnounceDirty: false, + stratumEnabled: false, + stratumBindPort: 3333, + stratumBindPortDirty: false, setupWallet: { address: null, seed_phrase: null, dev_verify_bypass: false, requires_peer: false }, setupNodeMode: "wallet", setupWalletMode: "create", @@ -420,6 +423,10 @@ window.iunaApp = function iunaApp() { if (!this.p2pBindPortDirty) { this.p2pBindPort = Number(this.config.p2p_bind_port || 9444); } + this.stratumEnabled = this.config.stratum_enabled === true; + if (!this.stratumBindPortDirty) { + this.stratumBindPort = Number(this.config.stratum_bind_port || 3333); + } if ( options.addressBookVersion === undefined || options.addressBookVersion >= this.addressBookVersion @@ -1601,6 +1608,68 @@ window.iunaApp = function iunaApp() { return `Restart iuna to open public P2P on ${configured || "the configured bind port"}.`; }, + async setStratumEnabled(enabled) { + const previous = this.stratumEnabled; + try { + this.stratumEnabled = enabled; + await this.postForm( + "/api/settings/stratum", + { enabled, bind_port: this.stratumBindPortValue() }, + enabled ? "Stratum endpoint setting saved" : "Stratum endpoint disabled" + ); + this.stratumBindPortDirty = false; + await this.refreshConfig(); + } catch (error) { + this.stratumEnabled = previous; + this.showFlash(error.message, "error"); + } + }, + + stratumBindPortValue() { + const port = Number(this.stratumBindPort); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error("Stratum bind port must be between 1 and 65535"); + } + return port; + }, + + stratumConfiguredBindAddr() { + const port = Number(this.config.stratum_bind_port || 3333); + if (!Number.isInteger(port) || port < 1 || port > 65535) return null; + return `0.0.0.0:${port}`; + }, + + stratumRestartRequired() { + const runtimeActive = this.config.stratum_runtime_enabled === true; + if (this.stratumEnabled !== runtimeActive) return true; + if (!this.stratumEnabled) return false; + const configured = this.stratumConfiguredBindAddr(); + return configured ? this.config.stratum_runtime_listen_addr !== configured : false; + }, + + stratumRestartMessage() { + if (!this.stratumRestartRequired()) return ""; + if (!this.stratumEnabled && this.config.stratum_runtime_enabled === true) { + return "Restart iuna to close the Stratum listener."; + } + const configured = this.stratumConfiguredBindAddr(); + return `Restart iuna to open Stratum on ${configured || "the configured bind port"}.`; + }, + + async saveStratumSettings() { + try { + await this.postForm( + "/api/settings/stratum", + { enabled: this.stratumEnabled, bind_port: this.stratumBindPortValue() }, + "Stratum endpoint setting saved" + ); + this.stratumBindPortDirty = false; + await this.refreshConfig(); + } catch (error) { + this.showFlash(error.message, "error"); + } + }, + async saveP2pAnnounce() { if (!this.p2pAcceptInbound) { this.showFlash("Enable public node before setting a public P2P address", "error"); @@ -3023,6 +3092,10 @@ window.iunaApp = function iunaApp() { return this.status.stratum?.listen_addr || "-"; }, + stratumRuntimeEnabled() { + return this.status.stratum?.enabled === true; + }, + stratumPoolUrl() { const listen = this.status.stratum?.listen_addr; if (!this.status.stratum?.enabled || !listen) return "-";