fix(bm13xx): hcn nonce range - #3
Draft
jayrmotta wants to merge 114 commits into
Draft
Conversation
Add cases for whole and fractional f64 difficulties in the existing display test, with comments grouping them by behavior.
from_f64 truncates to integer arithmetic before dividing. For values >= 1.0, the fractional part is dropped (200.5 becomes 200). For sub-1.0 values, the reciprocal is truncated (0.003 becomes 1/333, round-tripping as 0.003003). This test asserts the correct behavior and is marked #[should_panic] to document the known bug while keeping CI green.
Implement Div<f64> for U256 by decomposing the float into its exact rational form (mantissa * 2^exponent) via num-traits' Float::integer_decode() and performing the division entirely in integer arithmetic, preserving all 53 bits of mantissa precision. For negative exponents (fractional divisors), a 512-bit intermediate avoids overflow when left-shifting the dividend. Bring in num-traits 0.2 for the float decomposition.
The previous implementation truncated the f64 to integer arithmetic before dividing. For values >= 1.0, the fractional part was dropped (200.5 became 200). For sub-1.0 values, the reciprocal was truncated (0.003 became 1/333, round-tripping as 0.003003). Delegate to U256's Div<f64>, which preserves all 53 bits of mantissa precision. Remove the #[should_panic] annotation from the preceding round-trip test, since it now passes.
Stratum v1 pools may send difficulty as either integer or float values (e.g., 0.001 for low-difficulty vardiff). The previous implementation used as_u64() which rejected float values with "difficulty not a number". Switch the DifficultyChanged event payload and ProtocolState.difficulty field from u64 to f64, and parse incoming difficulty with as_f64(). Construct the internal Difficulty type via Difficulty::from_f64(). Add a guard for non-finite values as defense-in-depth (JSON has no NaN/Infinity representation, so serde_json never produces them). Add tests for float difficulty parsing.
The SourceState.difficulty field was u64, which truncated fractional difficulties (e.g. 0.001 became 0). Change to f64 to preserve the value received from the pool.
The 256-bit target-to-difficulty conversion can introduce tiny arithmetic residuals in the low-order digits. Round to 12 significant digits, derived from the conversion's error bound with four orders of magnitude of margin.
We sometimes add a test that asserts the correct behavior and mark it #[should_panic] to document a known bug while keeping CI green. The fix then removes the annotation in a separate commit. Document this pattern in the Testing section.
The old names were hard to follow because they described relative position in the clamp range rather than what each bound is for. Rename to measurement_target and flood_cap_target to match the constants they derive from.
compute_scheduler_target panics at low hashrates (~5 H/s). These hashrates are not normal for ASIC mining, but they can occur in degenerate cases and the scheduler must not panic regardless. The root cause is integer truncation in target_for_share_rate: the two share rate intervals truncate differently, inverting the clamp bounds. Replace the clamp ordering invariant test with a broader sweep of compute_scheduler_target across hashrates from 1 H/s to 100 TH/s. Mark with #[should_panic] to document the known bug.
hashes_in() used integer arithmetic, which truncated fractional results to zero at very low hashrates. This caused target_for_share_rate to return inconsistent values for the two share rate intervals, inverting the scheduler's clamp bounds and panicking. These hashrates are degenerate; no real hardware runs this slow. But a panic in the scheduler is never acceptable regardless of input. Switch hashes_in() to f64 so fractional hash counts (e.g., 0.5 hashes in 100 ms at 5 H/s) are preserved. Guard the case at <= 1.0 hashes per share with U256::MAX, keeping the division safe for all inputs above 1.0.
Clarify that HashRate is a per-miner type backed by u64, covering up to ~18.4 EH/s. Note the standard Rust overflow behavior (debug panics, release wraps).
The free function took (ShareRate, HashRate) and returned a Target. Moving it to a method on ShareRate follows the to_* conversion convention and parallels Difficulty::to_target().
Only used by expected_time_to_share; fold the formula in and remove the standalone function and its test.
Separate from the CI commit so reverting CI doesn't accidentally unignore .cache/, which may also be used by other tooling.
Add a Podman-based build toolchain image (build.Containerfile) with pinned Rust compiler, rustfmt, clippy, and just. The base image is pinned by digest for reproducibility. The justfile gains two new recipes: - build-image: builds the toolchain image, skipping if unchanged - in-container: runs any just recipe inside the toolchain image The image is tagged with a content hash of the Containerfile so build-image can detect staleness without rebuilding. This is necessary because podman save/load (used for CI caching) doesn't preserve layer cache metadata, so podman build would rebuild from scratch even with a loaded image. The content-hash tag lets `podman image exists` skip the build entirely. Downloaded crates are cached in .cache/ via bind mounts so they persist between container runs and are accessible to CI caching.
Run `just ci` on pushes to main and pull requests. The justfile's ci recipe delegates to `in-container "checks"`, keeping the workflow minimal and the justfile as the single source of truth for what CI does. The build toolchain image and cargo caches (compiled dependencies and downloaded crates) are cached between runs via actions/cache. Project crate artifacts are pruned before the target cache saves so only dependency objects are stored.
Update prerequisites to list just and optionally Podman. Replace the raw cargo commands in "Making Changes" with `just checks`, and add a "Reproducing CI locally" section explaining `just ci`.
The Drop impl on SerialInner called tcdrain(), a blocking syscall that waits for all queued output to be transmitted. If unread data sits in the kernel buffer with no reader, after a test or during shutdown, tcdrain blocks indefinitely. This hung transport::serial::tests::test_concurrent_read_write on macOS, where tcdrain on a PTY blocks forever with no reader. (On Linux, tcdrain returns immediately once the slave is closed, masking the bug.) Removing the Drop impl is safe for real hardware. write() copies data into the kernel's output buffer; once it returns, the data is in kernel space. The tty close path drains pending output for USB serial devices (controlled by the driver's closing_wait parameter, default 30 seconds). The tcdrain in Drop was redundant with what close() already provides for real devices. Fixes: 256foundation#46
Remove dependencies confirmed unused by cargo-machete analysis, as identified in the dependency audit (discussion 256foundation#8, issue 256foundation#29). mujina-miner: - sha2: bitcoin crate provides SHA-256 via bitcoin_hashes - hyper (direct): still available transitively via axum/reqwest - modular-bitfield: no usage in source mujina-dissect: - hex: no usage in source - thiserror: no usage in source - tracing: no usage in source (tracing-subscriber retained) Also removes sha2, hyper, and modular-bitfield from workspace dependencies as they are no longer referenced by any member. Eliminates ~15 exclusive transitive crates. Refs: 256foundation#29
Move tracing-journald from unconditional [dependencies] to [target.'cfg(target_os = "linux")'.dependencies] so it is not compiled on macOS. Gate the corresponding imports in tracing.rs (env, tracing_journald, prelude::*) behind cfg(target_os = "linux") to match, fixing unused-import warnings on macOS builds.
PR 256foundation#32 introduced per-import #[cfg] annotations to suppress macOS warnings, and subsequent cleanup in the same area was piling up more scattered cfg blocks. Consolidate all journald logic into a mod journald gated once on target_os = "linux", with a no-op stub for other platforms. Rename init_journald_or_stdout to init since callers should not need to know the logging strategy.
Merge queues are now enabled on the repo. Add the merge_group trigger so GitHub runs status checks on the temporary merge commits that the queue creates. See "Triggering merge group checks with GitHub Actions" in the GitHub docs on managing merge queues.
Restructure for a clearer learning path that teaches the workflow progressively rather than repeating information across sections. Cut sections that are redundant or too generic to be useful. Notable content changes: - Add draft PR guidance for sharing work in progress - Expand atomic commits section (revertability, bisectability, reviewability) - Fix commit examples to use conventional commit format with lowercase subjects matching actual repo practice - Distinguish feat/fix (behavioral) from other commit types - Add hyperlinks throughout (Telegram, rustup, good first issue, conventional commits, project docs) - Fix clone directory (mujina, not mujina-miner) - PR titles should follow conventional commit format
Delete the crate-level Error enum and Result alias. The enum variants were never pattern-matched; they just carried strings, duplicating what anyhow provides with better ergonomics. Convert all call sites to anyhow::Result with .context() and bail!() for error construction. Board and HashThread traits retain their typed error returns (BoardError, HashThreadError) unchanged; only the factory functions and transport layer move to anyhow.
Remove the BoardError and HashThreadError domain error enums. These types were never pattern-matched by callers; they served only as structured message formatters before being erased into anyhow::Error at every trait boundary. Rust convention is to start with anyhow and introduce typed errors (via thiserror) when a caller needs to match on a variant to take different action---retry logic, different shutdown behavior, etc. Since no caller distinguishes error variants today, the types are dead weight. If programmatic error matching becomes necessary, reintroducing a thiserror enum at that point is straightforward and will be better informed by actual requirements. Replace all BoardError and HashThreadError construction sites with anyhow context(), bail!(), and anyhow!() calls. Change Board and HashThread trait methods to return anyhow::Result. Internal helper functions (initialize_chip, task_to_job_full) also migrate to anyhow::Result.
Remove an earlier Chip trait, ChipError, ChipStats, MiningJob, and NonceResult that were never implemented. Retain ChipInfo which board implementations use during chip discovery.
Remove a leftover doc fragment from discover_chips that was incorrectly attached to send_config_command.
Use the project's tracing prelude wildcard import instead of importing individual macros from the tracing crate. This avoids import churn when logging levels change.
Consistent with guideline L.prelude, migrate remaining files to use crate::tracing::prelude::* instead of importing individual macros from the tracing crate.
The EMC2101 and EMC2101-R have distinct product IDs (0x16 and 0x28). Name the constants after the actual variants and include which one was detected in the debug log.
Add a sentence explaining that global trace is noisy and per-module targeting is the way to deal with it.
The Board trait encouraged each board to implement a struct that served double duty: it stored internal board state and acted as the interface to the backplane. The create/initialize/create_hash_threads method sequence encouraged storing intermediate state in Option fields between calls, with expect() to enforce sequencing the type system could not express. Replace the trait with a concrete BackplaneConnector struct returned by factory functions. The factory does all initialization in one shot and returns a fully-constructed value containing board info, hash threads, a telemetry channel, and an opaque shutdown future. The backplane destructures this and routes each part to where it belongs. Board-specific structs (BitaxeBoard, EmberOne00, CpuBoard) become private to their modules, responsible only for board state. Their shutdown logic is captured in a boxed future, so the backplane shuts down boards without knowing their internals. BoardRegistration moves from the board module to the API module, where it belongs. The board module defines how boards connect to the backplane; the API module defines how the API server connects to boards.
The BackplaneConnector pattern lets the factory function do all the work inline. The CpuBoard struct was a leftover from when the Board trait required an implementing type. Without the trait, the factory can create threads and build the connector directly.
The PLL calculation functions live in bm13xx/thread.rs but the tests that exercise them were in board/bitaxe.rs, which carried its own #[cfg(test)] copy. Delete the duplicates and move the tests next to the real code.
Move logic from BitaxeBoard::new() and BitaxeBoard::initialize() into create_from_usb(). The factory is now the single orchestrator. Rename BitaxeBoard to Bitaxe, holding only what the stats monitor and shutdown need. Fields consumed during initialization stay local to the factory. Extract init_power_controller, init_fan_controller, and discover_chips as free functions. Drop dead code (momentary_reset, send_config_commands, baud rate constants). The diff is large but mostly mechanical: code moves between methods and free functions, with minor adjustments to access patterns (self.field becomes a parameter or local).
Add run_monitor() on Bitaxe with a single polling interval. Each tick reads all sensors (EMC2101 and regulator), publishes BoardTelemetry, and logs a periodic summary. The EMC2101 is now a non-optional field on Bitaxe. Fan init is a hard error; if it fails, the board does not start. Shutdown uses a CancellationToken; the monitor runs the shutdown sequence (voltage off, hold reset) before exiting.
The EMC2101 reports specific raw values when the external temperature diode is open circuit (0x3F8) or shorted (0x3FF). Return these as errors instead of silently converting them to ~127 C temperature readings.
BitaxeAsicEnable now records when the ASIC was last taken out of reset. The monitor uses this to suppress temperature readings during the diode settle period after the ASIC powers on.
Classify temperature readings into four categories: I2C/diode faults, out-of-range, above-emergency, and normal. Any category except normal increments a consecutive bad-reading counter; after a threshold, force fan to 100% and shut down the board. On normal shutdown (cancellation), reduce fan to 25%. On thermal emergency, leave fan at 100%.
Reorganize the page to lead with what Mujina is and how to try it. Bring stale sections up to date. Outline: - Replace Overview and Supported Hardware with Vision and Current Status - Add a Quick Start that runs the CPU backend without hardware - Reorder Running: pool, then hardware without a pool, logging, API - Move the documentation links into a Further Reading index Current state: - Add macOS build requirements - Describe hardware support honestly: what works today, what's in progress, what's coming - Fix the run examples to work from the workspace root
The Saleae Logic 2 capture dissector has been retired. Drop the package and its workspace member entry. Resurrect from git history if needed later.
The mujina-dissect tool was the only caller of the `protocol` submodule's register-name lookup, transaction formatter, and PWM / temperature decoders. Hoist the register address table out to module scope as `regs` and remove the rest. Resurrect from git history if needed.
The mujina-dissect tool was the sole caller of the `PmbusValue` enum, the `parse_pmbus_value` family, and `TryFrom<u8>` for `PmbusCommand`. Also removes the now-unused `DEFAULT_VOUT_MODE` constant. Resurrect from git history if needed.
These module-doc TODOs talked about deduplicating CRC and frame validation logic against the dissector's copies. With mujina-dissect gone, there's nothing to dedupe against.
The previous document was written before most of the code existed and read as a design. The doc and code have diverged significantly since. Replace it with a short conceptual overview: three subsystems (Boards, Mining core, API), a diagram of how they connect, and brief sections on what each one does. Refer to the code for more. Module comments often have the next level of conceptual detail.
The test used 500 GH/s, but a Bitaxe Gamma's single BM1370 hashes at roughly 1 TH/s. The mismatch invites a future reader to wonder why a test named "bitaxe_gamma" uses a number that doesn't describe the hardware. Update the input and expected difficulty range so the test matches its name.
The per-thread capability estimate was a fixed ~5 MH/s regardless of duty cycle. It serves as the fallback before the per-thread estimator settles, and propagates through the scheduler's operational hashrate into, among other things, the forced-rate wrapper's share-target computation. Below 100% duty, the assumed hashrate exceeded actual, so shares arrived at the configured rate scaled by duty/100. Scale the estimate by duty_percent so the fallback reflects effective rate.
Declare a hash thread's expected hashrate at runtime instead of exposing a static estimate. Add a configure() command that readies a thread for work and makes it emit ExpectedHashRate, re-emitted whenever the expectation changes. Drop the hashrate_estimate capability. Drive source difficulty and the difficulty-too-high warning from the aggregated declared expecteds, not the old blend of measurement and static estimate. Measured hashrate drifts across these decision thresholds as boards ramp up and down, and the transients are awkward to handle; a rate the board declares for the near future stays put. Keep measured hashrate for telemetry and health. Broadcast to sources on topology change exactly as before. Only the value sent changes here, not the timing.
Send sources an updated hashrate when a thread reports its own expected hashrate, instead of when a new thread is added. A thread's first report is also when it becomes eligible for work and picks up the job the scheduler is holding for each source. Adding a thread now only registers and configures it. Compute that hashrate per source as a portion of the aggregate. Today there is one source, which gets the whole aggregate; registering a second logs a warning, since splitting is not implemented. Dividing the aggregate across sources later is a small, local change. When a source sends a job, skip any thread that has not yet reported its expected hashrate. Without that figure there is no sensible way to size its portion of the work, so it waits until its first report. Rename the periodic timer to reflect that it publishes only API telemetry.
At startup, boards enumerate and report their expected hashrate one by one. Broadcasting to sources on each report would ratchet pool difficulty up through a series of partial totals before settling on the real one. Hold the first broadcast until startup enumeration is provably complete, then send the assembled total once. Have each transport announce when it finishes its initial device scan. The backplane waits for that announcement from every transport, then signals the scheduler that enumeration is done. The signal travels the same path as the boards themselves, behind them, so by the time the scheduler sees it every starting board is already registered. Hold the scheduler's first broadcast until that signal arrives and every registered board has reported, or until a fallback timeout fires so a board that never reports cannot stall startup forever. While holding, the scheduler still records hashrates and assigns work; only the broadcast to sources waits.
Replace the conservative symmetric filter, which re-suggested only on a change past 2x in either direction, with an asymmetric deadband measured from the pool's difficulty floor. That floor is our own last suggestion. At least ckpool and HydraPool apply a client's suggested difficulty immediately and then hold it as a lower bound, raising difficulty above it on their own but never lowering past it. Re-suggest eagerly on a drop. Once the right difficulty falls below the floor the pool is pinned and cannot follow it down, and we no longer need the conservative filter's grace, because a declared drop in board hashrate is a real hardware change, not the luck a share-rate dip might be. Re-suggest lazily on a rise, since the pool climbs on its own, but loosen the old doubling threshold enough that adding a second identical board still registers. Start with these two factors and tune them as the policy proves out against real pools.
Rate-limit outgoing difficulty suggestions to at most one per interval. Send the first material change immediately and start a cooldown. Hold any material change that arrives during the cooldown. When the cooldown expires, re-test the latest hashrate and send once if it is still a material change, otherwise stop throttling. A burst of changes coalesces into one suggestion per interval, and a flurry that settles back inside the deadband sends nothing at all. Start with the interval value and tune it as the policy proves out.
The CPU thread declared its expected hashrate from a fixed 5 MH/s constant scaled by the duty cycle. That constant is wrong on most hardware: real throughput varies widely across CPUs, and even across build profiles the same chip differs by more than an order of magnitude, so the difficulty suggestions derived from it were off. Measure the rate instead. Hash a synthetic header flat-out for a short fixed window, then scale the result by the duty cycle. The probe runs on a blocking thread so it does not stall the async runtime, and the startup enumeration barrier already hides its latency.
Add -h/--help and -V/--version to the daemon, which previously parsed no arguments. The environment variables that configure it (pool, CPU miner, API address, USB, logging) were undocumented outside scattered source reads. --help now prints them grouped, each with a brief explanation, default, and example. Pull in clap now rather than hand-roll the parsing; the daemon will grow more flags. The descriptions come from a central registry that gathers wording currently spread across the modules that read each variable. Both the listing and where it lives are a stopgap, likely reworked once a comprehensive configuration system exists.
The error raised when CPU board setup fails referenced MUJINA_CPU_MINER, which nothing reads. The backend is enabled by MUJINA_CPUMINER_THREADS. Point the hint at that variable instead.
Compute suggested difficulty as a float and let it fall below 1 instead of clamping to a minimum of 1. A very slow worker such as the CPU miner needs a target it can actually meet. Clamping gave it difficulty 1, which it rarely produces shares for. The value stays an f64 through the suggest path, and the client decides the wire encoding. Whole difficulties still serialize as integers, the form pools expect, and only values below 1 go out as a fraction. Production boards run far above 1 and are unaffected.
…equency Register 0x10 (Hash Counting Number) limits the nonce range each big core searches before signaling job completion. The previous code sent a hardcoded empirical value captured from a 65–128-chip S21 Pro chain regardless of actual chain length or frequency, giving a single-chip Bitaxe Gamma far less than 100% nonce coverage. Replace it with NonceRangeConfig::computed(), which implements the formula from ESP-Miner bm1370.c BM1370_set_nonce_space: hcn_space = 2^32 / next_pow2(big_cores) / next_pow2(chip_count) hcn_max = hcn_space × 25 MHz / freq_mhz × 0.5 hcn_register = floor(hcn_max − 268) For one BM1370 at 525 MHz: 0xC2FB7, verified against the ESP-Miner reference. Also fix ChipType::BM1370.core_count() which returned 2048 (all small engines) instead of 128 (big cores, the correct HCN divisor per ESP-Miner device_config.h). Discovered chip count and core count now flow from bitaxe.rs discovery through BM13xxThread::new() and bm13xx_thread_actor() to initialize_chip(). Refs: 256foundation#72 Refs: bitaxeorg/ESP-Miner#420
Add module-level documentation to bm13xx/thread.rs explaining the four search-space dimensions (nonce × version × ntime × extranonce2), how each is rolled, and why the HCN register value must track frequency. Complements the NonceRangeConfig::computed() doc added in the previous commit. Refs: 256foundation#72 Refs: bitaxeorg/ESP-Miner#420
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.