diff --git a/.github/workflows/build_msrv.yml b/.github/workflows/build_msrv.yml index b6f6222a..5a0c0bd3 100644 --- a/.github/workflows/build_msrv.yml +++ b/.github/workflows/build_msrv.yml @@ -36,6 +36,8 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.85.0 + with: + components: rustfmt - name: Set up Node.js uses: actions/setup-node@v5 @@ -65,7 +67,7 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - key: cargo-deps-${{ hashFiles('Cargo.lock') }} + key: cargo-deps-${{ hashFiles('Cargo.lock', 'contrib/samples/Cargo.lock') }} restore-keys: cargo-deps- - name: Restore build artifacts @@ -83,7 +85,9 @@ jobs: key: codeql-packs-${{ hashFiles('contrib/codeql/codeql-pack.lock.yml') }} - name: Run linters - run: python3 contrib/lint/all_lint.py + run: python3 contrib/lint_all.py + env: + RUSTUP_TOOLCHAIN: 1.85.0 - name: Check PR commit messages if: github.event_name == 'pull_request' @@ -121,7 +125,7 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - key: cargo-deps-${{ hashFiles('Cargo.lock') }} + key: cargo-deps-${{ hashFiles('Cargo.lock', 'contrib/samples/Cargo.lock') }} restore-keys: cargo-deps- - name: Manage build artifacts diff --git a/.github/workflows/build_nightly.yml b/.github/workflows/build_nightly.yml index 14e443fc..745d331c 100644 --- a/.github/workflows/build_nightly.yml +++ b/.github/workflows/build_nightly.yml @@ -60,7 +60,7 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - key: cargo-deps-${{ hashFiles('Cargo.lock') }} + key: cargo-deps-${{ hashFiles('Cargo.lock', 'contrib/samples/Cargo.lock') }} restore-keys: cargo-deps- - name: Manage build artifacts @@ -71,8 +71,8 @@ jobs: restore-keys: | cargo-build-nightly-${{ runner.os }}-${{ runner.arch }}-${{ inputs.package }}- - - name: Format package - run: cargo fmt -p ${{ inputs.package }} --check + - name: Check formatting + run: python contrib/lint/lint_rust.py - name: Lint package run: cargo clippy -p ${{ inputs.package }} --features ${{ inputs.features }} --tests -- -D warnings diff --git a/.github/workflows/build_stable.yml b/.github/workflows/build_stable.yml index 85f1f59f..2b10dad4 100644 --- a/.github/workflows/build_stable.yml +++ b/.github/workflows/build_stable.yml @@ -62,7 +62,7 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - key: cargo-deps-${{ hashFiles('Cargo.lock') }} + key: cargo-deps-${{ hashFiles('Cargo.lock', 'contrib/samples/Cargo.lock') }} restore-keys: cargo-deps- - name: Manage build artifacts diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..eb62c5ee --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,77 @@ +name: Deploy docs + +on: + push: + branches: [develop] + workflow_dispatch: + +concurrency: + group: pages + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build site + runs-on: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2026-02-01 + targets: wasm32-unknown-unknown + + - name: Install wasm-pack + run: cargo install wasm-pack@0.15.0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: pyproject.toml + + - name: Install Python dependencies + run: pip install ".[dev]" + + - name: Manage cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: cargo-deps-${{ hashFiles('Cargo.lock', 'contrib/samples/Cargo.lock') }} + restore-keys: cargo-deps- + + - name: Build documentation + run: python contrib/build_docs.py build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: public + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-24.04-arm + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 296464d5..38bee55c 100644 --- a/.gitignore +++ b/.gitignore @@ -178,3 +178,16 @@ cython_debug/ # PyPI configuration file .pypirc +# Playwright browser binaries and test artifacts +.playwright/ +**/.playwright/ +.playwright-cli/ +**/playwright-report/ +**/test-results/ +**/e2e/ + +# Built site +public/ + +# WASM builds +*.wasm diff --git a/CLAUDE.md b/CLAUDE.md index bf114e8d..37549b55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,7 +93,7 @@ use some_external_crate; ### Directory layout -``` +```text pkgs// bench/ corpus/ @@ -103,6 +103,7 @@ pkgs// ``` `Cargo.toml` must set: + ```toml [package] name = "dash-" diff --git a/Cargo.lock b/Cargo.lock index 28dafd5a..83b302df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,12 +325,14 @@ dependencies = [ "bitcoin-internals", "bitcoin-units", "bitcoin_hashes", + "cfg-if", "dash-dev", "dash-num", "dash-pow", "dash-script", "dash-types", "hex-conservative", + "libm", "rstest", "serde", "serde_json", @@ -636,6 +638,12 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.12.1" diff --git a/README.md b/README.md index 5aed8f9b..97bfcf5c 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ ![Minimum Supported Rust Version](https://img.shields.io/badge/v1.85.0-msrv?style=flat&logo=rust&label=MSRV&color=orange) > [!WARNING] -> -> This SDK is in early stages of development and different crates may have different levels of conformance -> and testing rigour. The completeness of one crate does not imply the completeness of others. -> +> +> This SDK is in early stages of development and different crates may have different levels of conformance and +> testing rigour. The completeness of one crate does not imply the completeness of others. +> > As with any alternate implementation, unintended deviations from the reference implementation (i.e. -> [Dash Core](https://github.com/dashpay/dash)) are possible and must be accounted for as a risk when building -> on this SDK. If requirements demand strict conformance guarantees, it is recommended to interface with Dash Core -> through [RPC](https://docs.dash.org/en/22.0.0/docs/core/api/remote-procedure-calls.html), -> [REST](https://docs.dash.org/en/22.0.0/docs/core/api/http-rest.html) or -> [ZMQ](https://docs.dash.org/en/22.0.0/docs/core/api/zmq.html) instead. +> [Dash Core](https://github.com/dashpay/dash)) are possible and must be accounted for as a risk when building on +> this SDK. If requirements demand strict conformance guarantees, it is recommended to interface with Dash Core +> through [RPC](https://docs.dash.org/en/stable/docs/core/api/remote-procedure-calls.html), +> [REST](https://docs.dash.org/en/stable/docs/core/api/http-rest.html) or +> [ZMQ](https://docs.dash.org/en/stable/docs/core/api/zmq.html) instead. `base-sdk` is a parsing and stateless verification SDK for Dash's layer 1 blockchain. @@ -43,13 +43,14 @@ graph LR script[dash-script] pow[dash-pow] pkc[dash-pkc] - primitives[dash-primitives] end subgraph " " + primitives[dash-primitives] params[dash-params] p2p_core[dash-p2p-core] end + types --> num types --> script types --> pkc types --> primitives @@ -74,10 +75,11 @@ All crates support these standard features: | Feature | Description | Crates | |---------|-------------|--------| -| `default` | `no_std` + `alloc` (always enabled) | _All_ | +| _(baseline)_ | `no_std` + `alloc`, always available | _All_ | | `std` | Enable standard library support | _All_ | | `serde` | Enable serde serialization (where applicable) | [num](./pkgs/num), [p2p-core](./pkgs/p2p_core), [pkc](./pkgs/pkc), [primitives](./pkgs/primitives), [script](./pkgs/script), [types](./pkgs/types) | | `full` | Enables all non-conflicting features | _All_ | +| `_internal` | Access to package internals, reserved for testing and benchmarks. **Not part of API contract.** | _All_ | Specific crates define additional features: @@ -86,10 +88,11 @@ Specific crates define additional features: | `k256` | Enable secp256k1 support | [pkc](./pkgs/pkc) | | `bls_ietf` | Enable standard (IETF) BLS support | [pkc](./pkgs/pkc) | | `bls_chia` | Enable legacy (Chia) BLS support | [pkc](./pkgs/pkc) | -| `_internal` | Access to package internals, reserved for testing and benchmarks. **Not part of API contract.** | [pow](./pkgs/pow) | | `aes_hw` | Enable hardware-accelerated AES on supported platforms | [pow](./pkgs/pow) | | `simd` | Use SIMD backends (requires nightly) | [pow](./pkgs/pow) | ## License -Copyright © 2026-present, The Dash Core developers. See the accompanying file [LICENSE](./LICENSE) or https://opensource.org/license/MIT +Copyright © 2026-present, The Dash Core developers. See the accompanying file [LICENSE](./LICENSE) or + +https://opensource.org/license/MIT diff --git a/contrib/__init__.py b/contrib/__init__.py new file mode 100644 index 00000000..fc53dd38 --- /dev/null +++ b/contrib/__init__.py @@ -0,0 +1 @@ +"""Contributor tooling packages.""" diff --git a/contrib/build_docs.py b/contrib/build_docs.py new file mode 100755 index 00000000..c0527a5e --- /dev/null +++ b/contrib/build_docs.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Build the documentation site.""" + +from __future__ import annotations + +import http.server +import re +import shutil +import socket +import subprocess +import sys +from functools import partial +from pathlib import Path + +import rjsmin +from common import RETCODE_ERR, RETCODE_PASS, require_bin, root_dir + +SITE_DIR = Path("public") +PREVIEW_PORT = 8000 +WASM_SAMPLES_DIR = Path("contrib/samples") +WEB_ASSET_GLOBS = ("*.js", "*.css") + + +def _build_wasm_samples(root: Path, wasm_pack: str) -> None: + """Compile every WASM sample crate under *WASM_SAMPLES_DIR*.""" + samples = sorted((root / WASM_SAMPLES_DIR).glob("*/Cargo.toml")) + if not samples: + print("no WASM samples found", file=sys.stderr) + return + + import os + import tomllib + + toolchain_file = root / "rust-toolchain.toml" + with toolchain_file.open("rb") as f: + channel = tomllib.load(f)["toolchain"]["channel"] + env = { + **os.environ, + "RUSTUP_TOOLCHAIN": channel, + "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS": + "-C target-feature=+simd128", + } + + for cargo_toml in samples: + crate_dir = cargo_toml.parent + name = crate_dir.name + print(f"building WASM sample: {name}") + subprocess.run( # noqa: S603 + [ + wasm_pack, + "build", + str(crate_dir), + "--target", + "web", + "--out-dir", + "pkg", + "--no-default-features", + ], + check=True, + env=env, + ) + + +def _build_site(root: Path, zensical: str) -> None: + """Run zensical to build the documentation site.""" + subprocess.run( # noqa: S603 + [zensical, "build", "-f", str(root / "zensical.toml")], + check=True, + ) + + +def _copy_artifacts(root: Path) -> None: + """Copy WASM packages and web assets into the built site.""" + samples = sorted((root / WASM_SAMPLES_DIR).glob("*/Cargo.toml")) + site = root / SITE_DIR + + common_css = root / WASM_SAMPLES_DIR / "common.css" + if common_css.is_file(): + dest = site / "samples" / "common.css" + dest.parent.mkdir(parents=True, exist_ok=True) + print(f"copying {common_css} -> {dest}") + shutil.copy2(common_css, dest) + + for cargo_toml in samples: + crate_dir = cargo_toml.parent + name = crate_dir.name + dest_base = site / "samples" / name + + pkg_src = crate_dir / "pkg" + if pkg_src.is_dir(): + pkg_dest = dest_base / "pkg" + print(f"copying {pkg_src} -> {pkg_dest}") + if pkg_dest.exists(): + shutil.rmtree(pkg_dest) + shutil.copytree(pkg_src, pkg_dest) + + for pattern in WEB_ASSET_GLOBS: + for asset in crate_dir.glob(pattern): + dest = dest_base / asset.name + dest.parent.mkdir(parents=True, exist_ok=True) + print(f"copying {asset} -> {dest}") + shutil.copy2(asset, dest) + + +def _generate_pygments_css(site: Path) -> None: + """Append Pygments syntax-highlight CSS to the built style.css.""" + from pygments.formatters import HtmlFormatter + + # Lines Pygments prepends for line-numbered blocks (unused by this site). + skip_re = re.compile(r"^(pre |td\.linenos |span\.linenos )") + + parts = [] + for style_name, prefix in ( + ("github-light-default", ".md-typeset .highlight"), + ("github-dark-default", + '[data-md-color-scheme="slate"] .md-typeset .highlight'), + ): + fmt = HtmlFormatter(style=style_name) + for line in fmt.get_style_defs(prefix).splitlines(): + if not skip_re.match(line): + parts.append(line) + parts.append("") + + css_file = site / "style.css" + with css_file.open("a", encoding="utf-8") as f: + f.write("\n") + f.write("\n".join(parts)) + + print(f"appended Pygments CSS to {css_file}") + + +def _minify_js(site: Path) -> None: + """Minify JS files in the built sample directories.""" + samples_dir = site / "samples" + if not samples_dir.is_dir(): + return + for js in sorted(samples_dir.rglob("*.js")): + if js.parent.name == "pkg": + continue + print(f"minifying {js}") + original = js.read_text(encoding="utf-8") + minified = rjsmin.jsmin(original) + js.write_text(minified, encoding="utf-8") + + +def _build(root: Path) -> None: + """Run the full build pipeline.""" + wasm_pack = require_bin("wasm-pack") + zensical = require_bin("zensical") + + _build_wasm_samples(root, wasm_pack) + _build_site(root, zensical) + _copy_artifacts(root) + _generate_pygments_css(root / SITE_DIR) + _minify_js(root / SITE_DIR) + + +def _find_free_port(host: str, start: int) -> int: + """Return the first port from *start* upward that is not in use.""" + for port in range(start, 65536): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + sock.bind((host, port)) + return port + except OSError: + continue + raise RuntimeError("no free port found") + + +def _preview(root: Path) -> None: + """Build then serve the site on localhost for testing.""" + _build(root) + site = root / SITE_DIR + + handler = partial( + http.server.SimpleHTTPRequestHandler, + directory=str(site), + ) + host = "localhost" + port = _find_free_port(host, PREVIEW_PORT) + + http.server.HTTPServer.allow_reuse_address = True + srv = http.server.HTTPServer((host, port), handler) + print(f"serving {site} at http://{host}:{port}") + try: + srv.serve_forever() + except KeyboardInterrupt: + print("\ninterrupted, shutting down") + finally: + srv.server_close() + + +VERBS = {"build": _build, "preview": _preview} + + +def main() -> int: + """Entry point.""" + verb = sys.argv[1] if len(sys.argv) > 1 else "build" + action = VERBS.get(verb) + if action is None: + print( + f"unknown verb: {verb} (expected: {', '.join(VERBS)})", + file=sys.stderr, + ) + return RETCODE_ERR + + root = root_dir() + action(root) + return RETCODE_PASS + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as exc: + print(exc, file=sys.stderr) + sys.exit(RETCODE_ERR) diff --git a/contrib/codeql/codeql-config.yml b/contrib/codeql/codeql-config.yml new file mode 100644 index 00000000..309b2c37 --- /dev/null +++ b/contrib/codeql/codeql-config.yml @@ -0,0 +1,2 @@ +paths-ignore: + - public diff --git a/contrib/codeql/import.ql b/contrib/codeql/import.ql index 373c43dd..f3f4d095 100644 --- a/contrib/codeql/import.ql +++ b/contrib/codeql/import.ql @@ -23,7 +23,7 @@ import rust pragma[nomagic] private predicate fileCfgLines(File f, int cfgLine) { exists(string relPath, string content | - relPath = f.getAbsolutePath().regexpCapture(".*/pkgs/(.*)", 1) and + fileRelPath(f, relPath) and sourceLineContent(relPath, cfgLine, content) and content.matches("#[cfg%") ) @@ -43,6 +43,22 @@ predicate hasCfgGatedGap(File f, int startAfter, int endBefore) { ) } +/** Holds if `f` is inside a crate excluded from prelude rules. */ +private predicate isPreludeExcluded(File f) { + exists(string name | + name = preludeExcludeCrate() and + f.getAbsolutePath().matches("%/" + name + "/%") + ) +} + +/** Holds if `u` imports directly from `alloc` outside `prelude.rs`. */ +private predicate directAllocImport(Use u) { + usePrefix(u) = "alloc" and + not fileOf(u).getBaseName() = "prelude.rs" and + not fileOf(u).getAbsolutePath().matches("%/prelude/mod.rs") and + not isPreludeExcluded(fileOf(u)) +} + from Locatable item, string message where ( diff --git a/contrib/codeql/lib/files.qll b/contrib/codeql/lib/files.qll index 40c67712..52d5c9e2 100644 --- a/contrib/codeql/lib/files.qll +++ b/contrib/codeql/lib/files.qll @@ -43,6 +43,12 @@ Path rootPath(Path p) { not exists(result.getQualifier()) } +/** Materialises the repo-root-relative path for source files. */ +pragma[nomagic] +predicate fileRelPath(File f, string relPath) { + relPath = f.getAbsolutePath().regexpCapture(".*/(pkgs/.*)", 1) +} + /** Gets the first path segment of use declaration `u`. */ string usePrefix(Use u) { result = rootPath(u.getUseTree().getPath()).getSegment().getIdentifier().getText() diff --git a/contrib/codeql/lib/policy.qll b/contrib/codeql/lib/policy.qll index 201aed0c..4b6ea381 100644 --- a/contrib/codeql/lib/policy.qll +++ b/contrib/codeql/lib/policy.qll @@ -124,12 +124,6 @@ predicate isNonSerdeCrate(TypeItem t) { ) } -/** Materialises the regex capture for file-relative paths. */ -pragma[nomagic] -private predicate fileRelPath(File f, string relPath) { - relPath = f.getAbsolutePath().regexpCapture(".*/pkgs/(.*)", 1) -} - /** * Holds if `t` implements a serde trait via crate-qualified impl, * unqualified proc-macro expansion, or source-scanned match. @@ -211,11 +205,10 @@ predicate isSerdeExempt(TypeItem t) { not implementsTrait(t, "PartialEq") } -/** Holds if `u` imports directly from `alloc` outside `prelude.rs`. */ -predicate directAllocImport(Use u) { - usePrefix(u) = "alloc" and - not fileOf(u).getBaseName() = "prelude.rs" and - not fileOf(u).getAbsolutePath().matches("%/prelude/mod.rs") +/** Crate directory names excluded from prelude enforcement. */ +string preludeExcludeCrate() { + result = "samples/parser" or + result = "samples/solver" } /** Holds if file `f` is in a crate evaluated by decl ordering. */ diff --git a/contrib/common.py b/contrib/common.py new file mode 100644 index 00000000..0980f56b --- /dev/null +++ b/contrib/common.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Shared constants and helpers for lint scripts.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + +RETCODE_ERR = 1 +RETCODE_PASS = 0 +RETCODE_SKIP = 77 + + +def find_up( + start: Path, + predicate: Callable[[Path], bool], + label: str = "matching directory", +) -> Path: + """Walk upward from *start*, returning the first matching directory.""" + for directory in (start, *start.parents): + if predicate(directory): + return directory + raise FileNotFoundError(f"{label} not found above {start}") + + +def is_workspace_root(d: Path) -> bool: + """Return True if *d* looks like a Cargo workspace root.""" + cargo = d / "Cargo.toml" + return ( + cargo.is_file() + and "[workspace]" in cargo.read_text(encoding="utf-8") + and (d / "pkgs").is_dir() + ) + + +def require_bin(name: str, path: str | None = None) -> str: + """Return the path to *name* or raise FileNotFoundError.""" + result = shutil.which(name, path=path) + if result is None and os.name == "nt": + result = shutil.which(f"{name}.exe", path=path) + if result is None: + where = "in expected path" if path else "in PATH" + raise FileNotFoundError(f"error: {name} binary not found {where}") + return result + + +def root_dir() -> Path: + """Return the workspace root (directory containing Cargo.toml).""" + return find_up( + Path(__file__).resolve().parent, + is_workspace_root, + "workspace Cargo.toml", + ) + + +def usable_threads() -> int: + """Return a conservative thread count (total CPUs minus one).""" + return max(1, (os.cpu_count() or 2) - 1) + + +def usable_mem() -> int: + """Return half the physical RAM in MiB. + + Raises RuntimeError when physical RAM cannot be determined. + """ + total = _physical_ram_bytes() + return total // (2 * 1024 * 1024) + + +def _physical_ram_bytes() -> int: + """Return total physical RAM in bytes.""" + if sys.platform.startswith("linux"): + for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines(): + if line.startswith("MemTotal:"): + return int(line.split()[1]) * 1024 + raise RuntimeError("MemTotal not found in /proc/meminfo") + if sys.platform == "darwin": + try: + out = subprocess.check_output( + ["sysctl", "-n", "hw.memsize"], # noqa: S607 + ) + return int(out.strip()) + except ( + FileNotFoundError, subprocess.CalledProcessError, ValueError, + ) as exc: + raise RuntimeError( + "could not determine physical RAM on macOS", + ) from exc + if sys.platform == "win32": + try: + out = subprocess.check_output( + ["powershell", "-NoProfile", "-Command", # noqa: S607 + "(Get-CimInstance Win32_ComputerSystem)" + ".TotalPhysicalMemory"], + ).decode() + value = out.strip() + if value.isdigit(): + return int(value) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + try: + out = subprocess.check_output( + ["wmic", "computersystem", "get", # noqa: S607 + "TotalPhysicalMemory", "/value"], + ).decode() + for line in out.splitlines(): + if line.startswith("TotalPhysicalMemory="): + return int(line.split("=", 1)[1].strip()) + except (FileNotFoundError, subprocess.CalledProcessError, ValueError): + pass + raise RuntimeError("could not determine physical RAM on Windows") + raise RuntimeError(f"unsupported platform: {sys.platform}") diff --git a/contrib/js/eslint.config.mjs b/contrib/js/eslint.config.mjs index 37d75442..376c4a98 100644 --- a/contrib/js/eslint.config.mjs +++ b/contrib/js/eslint.config.mjs @@ -1,7 +1,53 @@ // @ts-check +const sharedRules = { + "arrow-body-style": ["error", "as-needed"], + curly: "error", + "default-case-last": "error", + "default-case": "error", + "dot-notation": "error", + eqeqeq: ["error", "always"], + "func-style": ["error", "declaration", { allowArrowFunctions: true }], + "no-async-promise-executor": "error", + "no-caller": "error", + "no-duplicate-case": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-fallthrough": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-iterator": "error", + "no-new-func": "error", + "no-param-reassign": "error", + "no-promise-executor-return": "error", + "no-proto": "error", + "no-redeclare": "error", + "no-return-await": "error", + "no-self-compare": "error", + "no-shadow": "error", + "no-throw-literal": "error", + "no-undef": "error", + "no-unmodified-loop-condition": "error", + "no-unreachable-loop": "error", + "no-unreachable": "error", + "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "no-use-before-define": ["error", { functions: false }], + "no-var": "error", + "object-shorthand": ["error", "always"], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-object-spread": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + quotes: ["error", "double", { avoidEscape: true }], + "require-await": "error", + semi: ["error", "always"], +}; + export default [ { + ignores: ["docs/**", "contrib/samples/**", "**/pkg/**"], languageOptions: { ecmaVersion: 2022, sourceType: "commonjs", @@ -13,49 +59,29 @@ export default [ setTimeout: "readonly", }, }, - rules: { - "arrow-body-style": ["error", "as-needed"], - curly: "error", - "default-case-last": "error", - "default-case": "error", - "dot-notation": "error", - eqeqeq: ["error", "always"], - "func-style": ["error", "declaration", { allowArrowFunctions: true }], - "no-async-promise-executor": "error", - "no-caller": "error", - "no-duplicate-case": "error", - "no-eval": "error", - "no-extend-native": "error", - "no-fallthrough": "error", - "no-implicit-coercion": "error", - "no-implied-eval": "error", - "no-iterator": "error", - "no-new-func": "error", - "no-param-reassign": "error", - "no-promise-executor-return": "error", - "no-proto": "error", - "no-redeclare": "error", - "no-return-await": "error", - "no-self-compare": "error", - "no-shadow": "error", - "no-throw-literal": "error", - "no-undef": "error", - "no-unmodified-loop-condition": "error", - "no-unreachable-loop": "error", - "no-unreachable": "error", - "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], - "no-use-before-define": ["error", { functions: false }], - "no-var": "error", - "object-shorthand": ["error", "always"], - "prefer-arrow-callback": "error", - "prefer-const": "error", - "prefer-object-spread": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "error", - quotes: ["error", "double", { avoidEscape: true }], - "require-await": "error", - semi: ["error", "always"], + rules: sharedRules, + }, + { + files: ["docs/**/*.js", "contrib/samples/**/*.js"], + ignores: ["**/pkg/**"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + console: "readonly", + document: "readonly", + navigator: "readonly", + performance: "readonly", + self: "readonly", + TextDecoder: "readonly", + TextEncoder: "readonly", + Uint8Array: "readonly", + URL: "readonly", + Worker: "readonly", + clearTimeout: "readonly", + setTimeout: "readonly", + }, }, + rules: sharedRules, }, ]; diff --git a/contrib/lint/common.py b/contrib/lint/common.py deleted file mode 100644 index 0980f56b..00000000 --- a/contrib/lint/common.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# coding: latin-1 - -# -# Copyright (c) 2026-present, The Dash Core developers -# SPDX-License-Identifier: MIT -# See the accompanying file LICENSE or https://opensource.org/license/MIT -# - -"""Shared constants and helpers for lint scripts.""" - -from __future__ import annotations - -import os -import shutil -import subprocess -import sys -from pathlib import Path -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable - -RETCODE_ERR = 1 -RETCODE_PASS = 0 -RETCODE_SKIP = 77 - - -def find_up( - start: Path, - predicate: Callable[[Path], bool], - label: str = "matching directory", -) -> Path: - """Walk upward from *start*, returning the first matching directory.""" - for directory in (start, *start.parents): - if predicate(directory): - return directory - raise FileNotFoundError(f"{label} not found above {start}") - - -def is_workspace_root(d: Path) -> bool: - """Return True if *d* looks like a Cargo workspace root.""" - cargo = d / "Cargo.toml" - return ( - cargo.is_file() - and "[workspace]" in cargo.read_text(encoding="utf-8") - and (d / "pkgs").is_dir() - ) - - -def require_bin(name: str, path: str | None = None) -> str: - """Return the path to *name* or raise FileNotFoundError.""" - result = shutil.which(name, path=path) - if result is None and os.name == "nt": - result = shutil.which(f"{name}.exe", path=path) - if result is None: - where = "in expected path" if path else "in PATH" - raise FileNotFoundError(f"error: {name} binary not found {where}") - return result - - -def root_dir() -> Path: - """Return the workspace root (directory containing Cargo.toml).""" - return find_up( - Path(__file__).resolve().parent, - is_workspace_root, - "workspace Cargo.toml", - ) - - -def usable_threads() -> int: - """Return a conservative thread count (total CPUs minus one).""" - return max(1, (os.cpu_count() or 2) - 1) - - -def usable_mem() -> int: - """Return half the physical RAM in MiB. - - Raises RuntimeError when physical RAM cannot be determined. - """ - total = _physical_ram_bytes() - return total // (2 * 1024 * 1024) - - -def _physical_ram_bytes() -> int: - """Return total physical RAM in bytes.""" - if sys.platform.startswith("linux"): - for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines(): - if line.startswith("MemTotal:"): - return int(line.split()[1]) * 1024 - raise RuntimeError("MemTotal not found in /proc/meminfo") - if sys.platform == "darwin": - try: - out = subprocess.check_output( - ["sysctl", "-n", "hw.memsize"], # noqa: S607 - ) - return int(out.strip()) - except ( - FileNotFoundError, subprocess.CalledProcessError, ValueError, - ) as exc: - raise RuntimeError( - "could not determine physical RAM on macOS", - ) from exc - if sys.platform == "win32": - try: - out = subprocess.check_output( - ["powershell", "-NoProfile", "-Command", # noqa: S607 - "(Get-CimInstance Win32_ComputerSystem)" - ".TotalPhysicalMemory"], - ).decode() - value = out.strip() - if value.isdigit(): - return int(value) - except (FileNotFoundError, subprocess.CalledProcessError): - pass - try: - out = subprocess.check_output( - ["wmic", "computersystem", "get", # noqa: S607 - "TotalPhysicalMemory", "/value"], - ).decode() - for line in out.splitlines(): - if line.startswith("TotalPhysicalMemory="): - return int(line.split("=", 1)[1].strip()) - except (FileNotFoundError, subprocess.CalledProcessError, ValueError): - pass - raise RuntimeError("could not determine physical RAM on Windows") - raise RuntimeError(f"unsupported platform: {sys.platform}") diff --git a/contrib/lint/common.py b/contrib/lint/common.py new file mode 120000 index 00000000..a11703ea --- /dev/null +++ b/contrib/lint/common.py @@ -0,0 +1 @@ +../common.py \ No newline at end of file diff --git a/contrib/lint/lint_codeql.py b/contrib/lint/lint_codeql.py index bfa717a8..d9f537d3 100755 --- a/contrib/lint/lint_codeql.py +++ b/contrib/lint/lint_codeql.py @@ -15,7 +15,6 @@ import contextlib import csv import datetime -import os import shutil import subprocess import sys @@ -53,7 +52,8 @@ def _discover_ql_sources(query_dir: Path) -> list[Path]: def _generate_source_lines( - source_root: Path, + repo_root: Path, + source_dirs: list[Path], query_dir: Path, ) -> Path: """Emit a raw source-line predicate for CodeQL queries. @@ -66,19 +66,22 @@ def _generate_source_lines( """ out = query_dir / "lib" / "source_lines.qll" rows: list[str] = [] - for rs_file in sorted(source_root.rglob("*.rs")): - rel = str(rs_file.relative_to(source_root)) - try: - text = rs_file.read_text(encoding="latin-1") - except OSError: - continue - for lineno, line in enumerate(text.splitlines(), 1): - if not any(kw in line for kw in _SOURCE_KEYWORDS): + for source_dir in source_dirs: + for rs_file in sorted(source_dir.rglob("*.rs")): + if "target" in rs_file.parts: continue - escaped = line.strip().replace("\\", "\\\\").replace('"', '\\"') - rows.append( - f' file = "{rel}" and line = {lineno} and content = "{escaped}"', - ) + rel = str(rs_file.relative_to(repo_root)) + try: + text = rs_file.read_text(encoding="latin-1") + except OSError: + continue + for lineno, line in enumerate(text.splitlines(), 1): + if not any(kw in line for kw in _SOURCE_KEYWORDS): + continue + escaped = line.strip().replace("\\", "\\\\").replace('"', '\\"') + rows.append( + f' file = "{rel}" and line = {lineno} and content = "{escaped}"', + ) body = "\n or\n".join(rows) if rows else " none()" timestamp = datetime.datetime.now(datetime.UTC).strftime( @@ -91,7 +94,7 @@ def _generate_source_lines( "/**\n" " * Holds if `line` in `file` contains look-out keywords.\n" " * `content` is the trimmed source text.\n" - " * File paths are relative to ``pkgs/``.\n" + " * File paths are relative to the repository root.\n" " */\n" "predicate sourceLineContent" "(string file, int line, string content) {\n" @@ -111,7 +114,7 @@ def _print_csv_diagnostics(results_path: Path) -> int: continue if len(row) < 6: raise ValueError(f"malformed CodeQL CSV row: {row!r}") - uri = Path("pkgs") / row[4].lstrip("/") + uri = Path(row[4].lstrip("/")) line = row[5] msg = row[3].replace("\n", " ") print(f"{uri}:{line}: {msg}", file=sys.stderr) @@ -190,7 +193,11 @@ def main(argv: list[str] | None = None) -> int: raise FileNotFoundError("no .ql queries found in contrib/codeql/") # Generate source-line data for queries that need raw text. - generated = _generate_source_lines(repo_root / "pkgs", query_dir) + source_dirs = [ + repo_root / "pkgs", + repo_root / "contrib" / "samples", + ] + generated = _generate_source_lines(repo_root, source_dirs, query_dir) subprocess.run( # noqa: S603 [codeql_bin, "query", "format", "-i", str(generated)], check=True, @@ -236,7 +243,7 @@ def main(argv: list[str] | None = None) -> int: if active_db.exists(): shutil.rmtree(active_db) active_db.parent.mkdir(parents=True, exist_ok=True) - db_env = {**os.environ, "CARGO_INCREMENTAL": "0"} + config_file = query_dir / "codeql-config.yml" result = subprocess.run( # noqa: S603 [ codeql_bin, @@ -244,12 +251,12 @@ def main(argv: list[str] | None = None) -> int: "create", str(active_db), "--language=rust", - f"--source-root={repo_root / 'pkgs'}", + "--build-mode=none", + f"--source-root={repo_root}", + f"--codescanning-config={config_file}", f"-j{usable_threads()}", - "--command=cargo check --features full,_internal", ], cwd=str(repo_root), - env=db_env, check=False, ) if result.returncode != RETCODE_PASS or not db_yml.is_file(): diff --git a/contrib/lint/lint_javascript.py b/contrib/lint/lint_javascript.py index 7ec2a56a..2ecbd4ce 100755 --- a/contrib/lint/lint_javascript.py +++ b/contrib/lint/lint_javascript.py @@ -16,7 +16,10 @@ ESLINT_VERSION = "9.39.3" -DEFAULT_TARGETS: tuple[str, ...] = (".github/scripts",) +DEFAULT_TARGETS: tuple[str, ...] = ( + ".github/scripts", + "contrib/samples", +) def main() -> int: diff --git a/contrib/lint/lint_markdown.py b/contrib/lint/lint_markdown.py new file mode 100755 index 00000000..a7da1259 --- /dev/null +++ b/contrib/lint/lint_markdown.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Lint Markdown files with pymarkdownlnt.""" + +from __future__ import annotations + +import subprocess +import sys + +from common import RETCODE_ERR, require_bin, root_dir + +DISABLED_RULES = "md025,md033,md041" + + +def main() -> int: + pymarkdown_bin = require_bin("pymarkdownlnt") + repo_root = root_dir() + result = subprocess.run( # noqa: S603 + [ + pymarkdown_bin, + "--disable-rules", + DISABLED_RULES, + "scan", + "--recurse", + "--respect-gitignore", + str(repo_root), + ], + capture_output=True, + check=False, + cwd=str(repo_root), + text=True, + ) + + prefix = str(repo_root) + "/" + for line in result.stdout.splitlines(): + print(line.replace(prefix, "")) + for line in result.stderr.splitlines(): + print(line.replace(prefix, ""), file=sys.stderr) + + return result.returncode + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as exc: + print(exc, file=sys.stderr) + sys.exit(RETCODE_ERR) diff --git a/contrib/lint/lint_rust.py b/contrib/lint/lint_rust.py new file mode 100644 index 00000000..343f8ddb --- /dev/null +++ b/contrib/lint/lint_rust.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Check Rust formatting across workspace manifests.""" + +from __future__ import annotations + +import subprocess +import sys + +from common import RETCODE_ERR, RETCODE_PASS, require_bin, root_dir + +CARGO_MANIFESTS: tuple[str, ...] = ( + "Cargo.toml", + "contrib/samples/Cargo.toml", +) + + +def main() -> int: + _ = require_bin("rustfmt") + cargo_bin = require_bin("cargo") + repo_root = root_dir() + + failed = False + for manifest in CARGO_MANIFESTS: + manifest_path = repo_root / manifest + print(f"checking formatting: {manifest}") + cmd = [cargo_bin, "fmt", "--check", "--all"] + cmd += ["--manifest-path", str(manifest_path)] + result = subprocess.run( # noqa: S603 + cmd, + check=False, + cwd=str(repo_root), + ) + if result.returncode != 0: + failed = True + + return RETCODE_ERR if failed else RETCODE_PASS + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as exc: + print(exc, file=sys.stderr) + sys.exit(RETCODE_ERR) diff --git a/contrib/lint/lint_semgrep.py b/contrib/lint/lint_semgrep.py index ee7b2bc8..44d46b11 100755 --- a/contrib/lint/lint_semgrep.py +++ b/contrib/lint/lint_semgrep.py @@ -27,7 +27,10 @@ def main() -> int: repo_root = root_dir() config_dir = repo_root / "contrib" / "semgrep" - target_dir = repo_root / "pkgs" + target_dirs = [ + repo_root / "pkgs", + repo_root / "contrib" / "samples", + ] configs: list[str] = [] for cfg in sorted(config_dir.glob("*.yml")): @@ -44,7 +47,7 @@ def main() -> int: "scan", *configs, "--error", - str(target_dir), + *[str(d) for d in target_dirs], ], check=False, ) diff --git a/contrib/lint/all_lint.py b/contrib/lint_all.py similarity index 97% rename from contrib/lint/all_lint.py rename to contrib/lint_all.py index fb96e753..d5111490 100755 --- a/contrib/lint/all_lint.py +++ b/contrib/lint_all.py @@ -137,7 +137,8 @@ def fmt_row(cells: tuple[str, ...], *, color: bool = False) -> str: async def _main() -> int: - lint_dir = Path(__file__).resolve().parent + contrib_dir = Path(__file__).resolve().parent + lint_dir = contrib_dir / "lint" scripts = _discover_linters(lint_dir) if not scripts: diff --git a/contrib/samples/Cargo.lock b/contrib/samples/Cargo.lock new file mode 100644 index 00000000..bad33b3f --- /dev/null +++ b/contrib/samples/Cargo.lock @@ -0,0 +1,329 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6a87a8367e7a4248c8dfd783c37ef492bca1307cd4b21b4cfad9cfd15bf060" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d7ca3dc8ff835693ad73bf1596240c06f974a31eeb3f611aaedf855f1f2725" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192615d454a4398c0e4654692610fcc7c9f67647e51ebec2786ae3b1f3f0fc35" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative", +] + +[[package]] +name = "bitcoin-units" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe8fafd73be14659c450deb64f90d2fd5a354fce366a01223a8319cf4d98b40" +dependencies = [ + "bitcoin-consensus-encoding 0.1.0", + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a45c2b41c457a9a9e4670422fcbdf109afb3b22bc920b4045e8bdfd788a3d" +dependencies = [ + "bitcoin-consensus-encoding 0.1.0", + "bitcoin-internals", + "hex-conservative", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "dash-num" +version = "0.0.0" +dependencies = [ + "bitcoin-consensus-encoding 0.2.0", + "dash-types", + "serde", +] + +[[package]] +name = "dash-pow" +version = "0.0.0" +dependencies = [ + "cfg-if", + "dash-num", +] + +[[package]] +name = "dash-primitives" +version = "0.0.0" +dependencies = [ + "bitcoin-consensus-encoding 0.2.0", + "bitcoin-internals", + "bitcoin-units", + "bitcoin_hashes", + "cfg-if", + "dash-num", + "dash-script", + "dash-types", + "hex-conservative", + "libm", + "serde", +] + +[[package]] +name = "dash-sample-parser" +version = "0.0.0" +dependencies = [ + "dash-primitives", + "dash-types", + "hex-conservative", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "dash-sample-solver" +version = "0.0.0" +dependencies = [ + "bitcoin-consensus-encoding 0.2.0", + "bitcoin-units", + "dash-num", + "dash-pow", + "dash-primitives", + "dash-types", + "hex-conservative", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "dash-script" +version = "0.0.0" +dependencies = [ + "base58ck", + "bitcoin-consensus-encoding 0.2.0", + "bitcoin_hashes", + "dash-types", + "serde", +] + +[[package]] +name = "dash-types" +version = "0.0.0" +dependencies = [ + "bitcoin-consensus-encoding 0.2.0", + "hex-conservative", + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/contrib/samples/Cargo.toml b/contrib/samples/Cargo.toml new file mode 100644 index 00000000..0075c000 --- /dev/null +++ b/contrib/samples/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +members = ["parser", "solver"] +resolver = "2" + +[workspace.lints.clippy] +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" +unwrap_used = "deny" + +[workspace.lints.rust] +keyword_idents = { level = "deny", priority = -1 } +missing_debug_implementations = "deny" +non_ascii_idents = "deny" +rust_2021_incompatible_closure_captures = "deny" +rust_2021_incompatible_or_patterns = "deny" +unsafe_code = "deny" +unused_must_use = "deny" + +[profile.release] +lto = true +opt-level = "s" diff --git a/contrib/samples/common.css b/contrib/samples/common.css new file mode 100644 index 00000000..f9aebd2b --- /dev/null +++ b/contrib/samples/common.css @@ -0,0 +1,59 @@ +@import "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap"; + +.sample-root { + font-size: 14px; +} + +.sample-root select, +.sample-root button { + padding: 0.4em 0.8em; + font-family: inherit; + font-size: 1em; + font-weight: 600; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + cursor: pointer; + background: var(--md-code-bg-color); + color: var(--md-default-fg-color); +} + +.sample-root .btn-primary { + background: var(--md-accent-fg-color); + color: #fff; + border-color: var(--md-accent-fg-color); +} + +.sample-root .btn-primary:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.sample-root input:focus, +.sample-root textarea:focus { + outline: none; + border-color: var(--md-accent-fg-color); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-accent-fg-color) 20%, transparent); +} + +.sample-root .sample-error { + color: #cf222e; +} + +[data-md-color-scheme="slate"] .sample-root .sample-error { + color: #f85149; +} + +.sample-root .admonition.warning { + margin: 0.5em 0; + padding: 0.6em 0.8em; + border-left: 3px solid #d4a017; + border-radius: 4px; + background: #fef9e7; + color: #6e5b00; +} + +[data-md-color-scheme="slate"] .sample-root .admonition.warning { + background: #3b3000; + color: #fcd34d; + border-left-color: #ca8a04; +} diff --git a/contrib/samples/parser/Cargo.toml b/contrib/samples/parser/Cargo.toml new file mode 100644 index 00000000..e4982605 --- /dev/null +++ b/contrib/samples/parser/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "dash-sample-parser" +version = "0.0.0" +edition = "2021" +license = "MIT" +publish = false + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +name = "demo_parser" +path = "parser.rs" +crate-type = ["cdylib"] + +[features] +default = [] +std = [] +full = ["std"] + +[dependencies] +dash-primitives = { version = "0.0.0", path = "../../../pkgs/primitives", default-features = false, features = [ + "serde", +] } +dash-types = { version = "0.0.0", path = "../../../pkgs/types", default-features = false } +hex-conservative = { version = "0.3", default-features = false, features = ["alloc"] } +serde_json = { version = "1", default-features = false, features = ["alloc"] } +wasm-bindgen = "0.2" + +[lints] +workspace = true diff --git a/contrib/samples/parser/index.js b/contrib/samples/parser/index.js new file mode 100644 index 00000000..9a993561 --- /dev/null +++ b/contrib/samples/parser/index.js @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2026-present, The Dash Core developers + * SPDX-License-Identifier: MIT + * See the accompanying file LICENSE or https://opensource.org/license/MIT + */ + +// @ts-check + +import init, { parse_block_hex, parse_tx_hex } from "./pkg/demo_parser.js"; + +/** @type {Record string>} */ +const PARSERS = { block: parse_block_hex, transaction: parse_tx_hex }; + +/** + * @param {HTMLElement} label + * @param {HTMLElement} children + */ +function toggleNode(label, children) { + const open = children.style.display !== "none"; + children.style.display = open ? "none" : ""; + label.classList.toggle("closed", open); +} + +/** + * @param {string} key + * @param {unknown} value + * @returns {HTMLElement} + */ +function buildTree(key, value) { + if (value !== null && typeof value === "object") { + const isArr = Array.isArray(value); + const entries = isArr + ? value.map((/** @type {unknown} */ v, /** @type {number} */ i) => [String(i), v]) + : Object.entries(/** @type {Record} */ (value)); + const suffix = isArr ? ` [${value.length}]` : ""; + + const node = document.createElement("div"); + node.className = "tree-node"; + + const label = document.createElement("span"); + label.className = "tree-label"; + label.textContent = `${key}${suffix}`; + node.appendChild(label); + + const children = document.createElement("div"); + children.className = "tree-children"; + for (const [k, v] of entries) { + children.appendChild(buildTree(k, v)); + } + node.appendChild(children); + + label.addEventListener("click", () => { + toggleNode(label, children); + }); + + return node; + } + + const div = document.createElement("div"); + div.className = "leaf"; + + const keySpan = document.createElement("span"); + keySpan.className = "key"; + keySpan.textContent = `${key}: `; + div.appendChild(keySpan); + + const valSpan = document.createElement("span"); + if (value === null) { + valSpan.className = "val-null"; + valSpan.textContent = "null"; + } else if (typeof value === "boolean") { + valSpan.className = "val-bool"; + valSpan.textContent = String(value); + } else if (typeof value === "number") { + valSpan.className = "val-num"; + valSpan.textContent = String(value); + } else { + valSpan.className = "val-str"; + valSpan.textContent = `"${value}"`; + } + div.appendChild(valSpan); + + return div; +} + +/** @param {string} id @returns {HTMLElement} */ +const $ = (id) => /** @type {HTMLElement} */ (document.getElementById(id)); + +const hexInput = /** @type {HTMLTextAreaElement} */ ($("hex-input")); +const typeSelect = /** @type {HTMLSelectElement} */ ($("type-select")); +const parseBtn = /** @type {HTMLButtonElement} */ ($("parse-btn")); +const clearBtn = /** @type {HTMLButtonElement} */ ($("clear-btn")); +const errorMsg = $("error-msg"); +const warnings = $("warnings"); +const output = $("output"); + +/** + * @param {string[]} msgs + */ +function renderWarnings(msgs) { + warnings.replaceChildren(); + for (const msg of msgs) { + const adm = document.createElement("div"); + adm.className = "admonition warning"; + adm.textContent = msg; + warnings.appendChild(adm); + } +} + +function handleParse() { + errorMsg.textContent = ""; + warnings.replaceChildren(); + output.replaceChildren(); + + const hex = hexInput.value.trim(); + if (!hex) { + errorMsg.textContent = "Paste hex data above."; + return; + } + + const parser = PARSERS[typeSelect.value]; + if (!parser) { + errorMsg.textContent = `Unknown type: ${typeSelect.value}`; + return; + } + + try { + const result = JSON.parse(parser(hex)); + const data = /** @type {Record} */ (result.data || result); + const warns = /** @type {string[]} */ (result.warnings || []); + + if (warns.length > 0) { + renderWarnings(warns); + } + + for (const [k, v] of Object.entries(data)) { + output.appendChild(buildTree(k, v)); + } + } catch (err) { + errorMsg.textContent = String(err); + } +} + +function handleClear() { + hexInput.value = ""; + errorMsg.textContent = ""; + warnings.replaceChildren(); + output.replaceChildren(); +} + +init() + .then(() => { + parseBtn.disabled = false; + parseBtn.addEventListener("click", handleParse); + clearBtn.addEventListener("click", handleClear); + }) + .catch((err) => { + errorMsg.textContent = `Failed to load WASM module: ${err}`; + }); diff --git a/contrib/samples/parser/parser.rs b/contrib/samples/parser/parser.rs new file mode 100644 index 00000000..83053a09 --- /dev/null +++ b/contrib/samples/parser/parser.rs @@ -0,0 +1,107 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Block and transaction parser. + +#![no_std] + +extern crate alloc; + +use dash_primitives::{Block, BlockHeader, Transaction}; +use dash_types::codec::BaseCodec; +use hex_conservative::FromHex; +use serde_json::Value; +use wasm_bindgen::prelude::*; + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use alloc::{format, vec}; + +/// Replace the hex `extra_payload` field with the decoded special payload +/// structure. +fn enrich_tx(tx: &Transaction, val: &mut Value) -> Option { + let obj = match val.as_object_mut() { + Some(o) => o, + None => return None, + }; + match tx.decode_payload() { + Some(Ok(payload)) => { + if let Ok(v) = serde_json::to_value(&payload) { + obj.insert("extra_payload".into(), v); + } + None + } + Some(Err(e)) => Some(format!("{e}")), + None => None, + } +} + +/// Enrich all transactions inside a serialized block value. +fn enrich_block(block: &Block, val: &mut Value) -> Vec { + let mut warnings = Vec::new(); + let txs = match val.get_mut("transactions").and_then(Value::as_array_mut) { + Some(a) => a, + None => return warnings, + }; + for (i, (tx, json_tx)) in block.transactions.iter().zip(txs.iter_mut()).enumerate() { + if let Some(msg) = enrich_tx(tx, json_tx) { + warnings.push(format!("tx {i}: {msg}")); + } + } + warnings +} + +/// Build the JSON envelope +fn envelope(data: Value, warnings: Vec) -> Result { + let w: Vec = warnings.into_iter().map(Value::String).collect(); + let mut map = serde_json::Map::new(); + map.insert("data".into(), data); + map.insert("warnings".into(), Value::Array(w)); + serde_json::to_string_pretty(&Value::Object(map)).map_err(|e| format!("failed to serialize to JSON: {e}")) +} + +/// Parses a hex-encoded raw block or block header and returns a JSON string. +/// Inputs of exactly 80 bytes are decoded as a block header; longer inputs are +/// decoded as a full block. +#[wasm_bindgen] +pub fn parse_block_hex(hex_str: &str) -> Result { + let bytes = Vec::::from_hex(hex_str).map_err(|e| format!("invalid hex: {e}"))?; + + if bytes.is_empty() { + return Err("no data provided".to_string()); + } + + if bytes.len() == 80 { + let header = BlockHeader::decode(&mut &bytes[..]).map_err(|e| format!("failed to decode block header: {e}"))?; + let val = serde_json::to_value(&header).map_err(|e| format!("failed to serialize to JSON: {e}"))?; + return envelope(val, vec![]); + } + + let block = Block::decode(&mut &bytes[..]).map_err(|e| format!("failed to decode block: {e}"))?; + let mut val = serde_json::to_value(&block).map_err(|e| format!("failed to serialize to JSON: {e}"))?; + let warnings = enrich_block(&block, &mut val); + + envelope(val, warnings) +} + +/// Parses a hex-encoded raw transaction and returns a JSON string. +#[wasm_bindgen] +pub fn parse_tx_hex(hex_str: &str) -> Result { + let bytes = Vec::::from_hex(hex_str).map_err(|e| format!("invalid hex: {e}"))?; + + if bytes.is_empty() { + return Err("no data provided".to_string()); + } + + let tx = Transaction::decode(&mut &bytes[..]).map_err(|e| format!("failed to decode transaction: {e}"))?; + let mut val = serde_json::to_value(&tx).map_err(|e| format!("failed to serialize to JSON: {e}"))?; + let warnings = match enrich_tx(&tx, &mut val) { + Some(msg) => vec![msg], + None => vec![], + }; + + envelope(val, warnings) +} diff --git a/contrib/samples/parser/style.css b/contrib/samples/parser/style.css new file mode 100644 index 00000000..9a200db6 --- /dev/null +++ b/contrib/samples/parser/style.css @@ -0,0 +1,78 @@ +#wasm-parser textarea { + width: 100%; + font-family: 'JetBrains Mono', ui-monospace, Menlo, + Consolas, 'Liberation Mono', monospace; + font-size: 1em; + padding: 0.6em; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + resize: vertical; + background: var(--md-code-bg-color); + color: var(--md-default-fg-color); +} + +#wasm-parser textarea::placeholder { + color: var(--md-default-fg-color--light); +} + +#wasm-parser .parser-actions { + margin-top: 0.5em; + display: flex; + gap: 0.5em; + align-items: center; +} + +#wasm-parser #error-msg { + margin-top: 0.5em; + min-height: 1.2em; +} + +#wasm-parser #output { + margin-top: 1em; + font-family: 'JetBrains Mono', ui-monospace, Menlo, + Consolas, 'Liberation Mono', monospace; + line-height: 1.5; +} + +#wasm-parser .tree-children { + margin-left: 1em; + padding-left: 0.75em; + border-left: 1px solid var(--md-default-fg-color--lightest); +} + +#wasm-parser .tree-label { + cursor: pointer; + font-weight: 600; + color: var(--md-default-fg-color); + padding: 0.1em 0; + display: block; + user-select: none; +} + +#wasm-parser .tree-label::before { + content: "\25be\00a0"; + font-size: 0.85em; +} + +#wasm-parser .tree-label.closed::before { + content: "\25b8\00a0"; +} + +#wasm-parser .leaf { + margin-left: 1em; + padding: 0.1em 0; +} + +#wasm-parser .key { + color: var(--md-default-fg-color); +} + +#wasm-parser .val-str { color: #116329; } +#wasm-parser .val-num { color: #0550ae; } +#wasm-parser .val-bool { color: #8250df; } +#wasm-parser .val-null { color: #b1bac4; } + +[data-md-color-scheme="slate"] #wasm-parser .val-str { color: #7ee787; } +[data-md-color-scheme="slate"] #wasm-parser .val-num { color: #79c0ff; } +[data-md-color-scheme="slate"] #wasm-parser .val-bool { color: #d2a8ff; } +[data-md-color-scheme="slate"] #wasm-parser .val-null { color: #4a5568; } diff --git a/contrib/samples/solver/Cargo.toml b/contrib/samples/solver/Cargo.toml new file mode 100644 index 00000000..f07a3d8f --- /dev/null +++ b/contrib/samples/solver/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "dash-sample-solver" +version = "0.0.0" +edition = "2021" +license = "MIT" +publish = false + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +name = "demo_solver" +path = "solver.rs" +crate-type = ["cdylib"] + +[features] +default = [] +std = [] +full = ["std"] + +[dependencies] +bitcoin-consensus-encoding = { version = "0.2", default-features = false, features = [ + "alloc", +] } +bitcoin-units = { version = "0.3", default-features = false, features = [ + "alloc", +] } +dash-num = { version = "0.0.0", path = "../../../pkgs/num", default-features = false } +dash-pow = { version = "0.0.0", path = "../../../pkgs/pow", default-features = false, features = ["simd"] } +dash-primitives = { version = "0.0.0", path = "../../../pkgs/primitives", default-features = false } +dash-types = { version = "0.0.0", path = "../../../pkgs/types", default-features = false } +hex-conservative = { version = "0.3", default-features = false, features = ["alloc"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"] } +serde_json = { version = "1", default-features = false, features = ["alloc"] } +wasm-bindgen = "0.2" + +[lints] +workspace = true diff --git a/contrib/samples/solver/index.js b/contrib/samples/solver/index.js new file mode 100644 index 00000000..2921e1ee --- /dev/null +++ b/contrib/samples/solver/index.js @@ -0,0 +1,501 @@ +/** + * Copyright (c) 2026-present, The Dash Core developers + * SPDX-License-Identifier: MIT + * See the accompanying file LICENSE or https://opensource.org/license/MIT + */ + +// @ts-check + +import init, { merkle_root } from "./pkg/demo_solver.js"; + +const RE_HEX = /^[0-9a-fA-F]+$/; +const RE_DIGITS = /^[0-9]+$/; + +// Coinbase scriptSig embedded in the Dash genesis block. +const SCRIPT_SIG = + "04ffff001d01044c5957697265642030392f4a616e2f3230313420546865" + + "204772616e64204578706572696d656e7420476f6573204c6976653a204f" + + "76657273746f636b2e636f6d204973204e6f7720416363657074696e6720" + + "426974636f696e73"; + +// Pay-to-pubkey output script used in the genesis coinbase. +const SCRIPT_PUBKEY = + "41040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4" + + "d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070" + + "ac7b03a9ac"; + +// Known genesis block header fields per network. +const PRESETS = { + mainnet: { + time: 1390095618, + bits: 0x1e0ffff0, + nonce: 28917698, + hash: "00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6", + }, + testnet: { + time: 1390666206, + bits: 0x1e0ffff0, + nonce: 3861367235, + hash: "00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c", + }, +}; + +// Default coinbase reward in duffs (50 DASH). +const AMOUNT_DUFFS = "5000000000"; +// Genesis block has no parent. +const PREV_HASH = "0".repeat(64); +// Full u32 nonce space (2^32). +const NONCE_MAX = 0x100000000; +// Time to wait for a closer nonce from a higher-priority thread before accepting. +const GRACE_TIMEOUT_MS = 30 * 1000; +// 1 DASH = 100,000,000 duffs. +const DUFFS_PER_DASH = 1e8; +// Minimum worker threads; divisor applied to navigator.hardwareConcurrency. +const MIN_THREADS = 2; +const THREAD_DIVISOR = 4; + +/** @param {string} id @returns {HTMLElement} */ +const $ = (id) => /** @type {HTMLElement} */ (document.getElementById(id)); + +const networkSel = /** @type {HTMLSelectElement} */ ($("gen-network")); +const amountInput = /** @type {HTMLInputElement} */ ($("gen-amount")); +const amountToggle = /** @type {HTMLButtonElement} */ ($("gen-amount-toggle")); +const sigInput = /** @type {HTMLTextAreaElement} */ ($("gen-scriptsig")); +const sigToggle = /** @type {HTMLButtonElement} */ ($("gen-sig-toggle")); +const pkInput = /** @type {HTMLTextAreaElement} */ ($("gen-scriptpubkey")); +const timeInput = /** @type {HTMLInputElement} */ ($("gen-time")); +const bitsInput = /** @type {HTMLInputElement} */ ($("gen-bits")); +const bitsToggle = /** @type {HTMLButtonElement} */ ($("gen-bits-toggle")); +const versionInput = /** @type {HTMLInputElement} */ ($("gen-version")); +const nonceInput = /** @type {HTMLInputElement} */ ($("gen-nonce")); +const merkleOutput = /** @type {HTMLInputElement} */ ($("gen-merkle")); +const hashOutput = /** @type {HTMLInputElement} */ ($("gen-hash")); +const solveBtn = /** @type {HTMLButtonElement} */ ($("gen-solve")); +const resetBtn = /** @type {HTMLButtonElement} */ ($("gen-reset")); +const solveInfo = $("gen-solve-info"); +const matchNote = $("gen-match-note"); +const coinbaseError = $("gen-coinbase-error"); +const headerError = $("gen-header-error"); +const versionWarn = $("gen-version-warn"); + +/** @type {"hex"|"ascii"} */ +let sigMode = "hex"; +/** @type {"hex"|"dec"} */ +let bitsMode = "hex"; +/** @type {"dash"|"duffs"} */ +let amountMode = "duffs"; +/** @type {Worker[]} */ +let workers = []; +let graceTimer = 0; +let wasmReady = false; + +function currentPreset() { + return PRESETS[/** @type {keyof PRESETS} */ (networkSel.value)]; +} + +/** @param {number} khs */ +function fmtRate(khs) { + if (khs >= 1000) { + return `${(khs / 1000).toFixed(1)} Mh/s`; + } + return `${khs.toFixed(1)} Kh/s`; +} + +function stopSolving() { + for (const w of workers) { + w.terminate(); + } + workers = []; + clearTimeout(graceTimer); + graceTimer = 0; + solveBtn.disabled = false; + solveBtn.textContent = "Solve"; + nonceInput.readOnly = false; +} + +function loadPreset() { + stopSolving(); + const p = currentPreset(); + amountMode = "duffs"; + amountToggle.textContent = "duffs"; + amountInput.value = AMOUNT_DUFFS; + sigMode = "hex"; + sigToggle.textContent = "hex"; + sigInput.value = SCRIPT_SIG; + pkInput.value = SCRIPT_PUBKEY; + timeInput.value = String(p.time); + bitsMode = "hex"; + bitsToggle.textContent = "hex"; + bitsInput.value = `0x${p.bits.toString(16).padStart(8, "0")}`; + versionInput.value = "1"; + nonceInput.value = String(p.nonce); + hashOutput.value = ""; + solveInfo.textContent = ""; + matchNote.classList.add("hidden"); + coinbaseError.textContent = ""; + coinbaseError.classList.add("hidden"); + headerError.textContent = ""; + headerError.classList.add("hidden"); + checkVersion(); + updateMerkleRoot(); +} + +/** @param {HTMLElement} el @param {string} msg */ +function showError(el, msg) { + el.textContent = msg; + el.classList.remove("hidden"); +} + +function checkVersion() { + const v = parseInt(versionInput.value, 10); + versionWarn.classList.toggle("hidden", v === 1 || isNaN(v)); +} + +/** @param {string} hex @returns {boolean} */ +function isValidHex(hex) { + return hex.length > 0 && hex.length % 2 === 0 && RE_HEX.test(hex); +} + +/** @param {string} v @returns {boolean} */ +function isU32(v) { + const n = Number(v); + return Number.isInteger(n) && n >= 0 && n <= 0xFFFFFFFF; +} + +/** @param {string} v @returns {boolean} */ +function isI32(v) { + const n = Number(v); + return Number.isInteger(n) && n >= -0x80000000 && n <= 0x7FFFFFFF; +} + +function updateMerkleRoot() { + if (!wasmReady) { + return; + } + const sig = getScriptSigHex(); + const pk = pkInput.value.trim(); + const amount = getAmountDuffs(); + if (!isValidHex(sig) || !isValidHex(pk)) { + merkleOutput.value = ""; + return; + } + try { + merkleOutput.value = merkle_root(sig, pk, amount); + } catch { + merkleOutput.value = ""; + } +} + +function validate() { + coinbaseError.textContent = ""; + coinbaseError.classList.add("hidden"); + headerError.textContent = ""; + headerError.classList.add("hidden"); + if (!isValidHex(getScriptSigHex())) { + showError(coinbaseError, "Signature script: invalid hex (must be even-length hex characters)"); + return false; + } + if (!isValidHex(pkInput.value.trim())) { + showError(coinbaseError, "Output script: invalid hex (must be even-length hex characters)"); + return false; + } + const amount = getAmountDuffs(); + if (!/^\d+$/.test(amount)) { + showError(coinbaseError, "Amount: invalid value"); + return false; + } + if (!isU32(String(parseBits())) || parseBits() === 0) { + showError(headerError, "Difficulty: invalid value"); + return false; + } + if (!isU32(timeInput.value.trim())) { + showError(headerError, "Timestamp: invalid value"); + return false; + } + if (!isI32(versionInput.value.trim())) { + showError(headerError, "Version: invalid value"); + return false; + } + if (!isU32(nonceInput.value.trim())) { + showError(headerError, "Nonce: invalid value"); + return false; + } + return true; +} + +/** @returns {string} */ +function getScriptSigHex() { + if (sigMode === "hex") { + return sigInput.value.trim(); + } + const encoder = new TextEncoder(); + const bytes = encoder.encode(sigInput.value); + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +} + +/** @returns {number} */ +function parseBits() { + const v = bitsInput.value.trim(); + if (bitsMode === "hex") { + return RE_HEX.test(v) ? parseInt(v, 16) : NaN; + } + return RE_DIGITS.test(v) ? parseInt(v, 10) : NaN; +} + +/** @returns {string} */ +function getAmountDuffs() { + const v = amountInput.value.trim(); + if (amountMode === "duffs") { + return v; + } + return String(Math.round(parseFloat(v) * DUFFS_PER_DASH)); +} + +function toggleAmount() { + const duffs = getAmountDuffs(); + if (amountMode === "duffs") { + amountMode = "dash"; + amountToggle.textContent = "DASH"; + amountInput.value = (Number(duffs) / DUFFS_PER_DASH).toFixed(8); + } else { + amountMode = "duffs"; + amountToggle.textContent = "duffs"; + amountInput.value = duffs; + } +} + +function toggleSig() { + if (sigMode === "hex") { + const hex = sigInput.value.trim(); + if (!isValidHex(hex)) { + return; + } + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + const decoder = new TextDecoder("utf-8", { fatal: false }); + sigInput.value = decoder.decode(bytes); + sigMode = "ascii"; + sigToggle.textContent = "ASCII"; + } else { + const hex = getScriptSigHex(); + sigInput.value = hex; + sigMode = "hex"; + sigToggle.textContent = "hex"; + } +} + +function toggleBits() { + const bits = parseBits(); + if (isNaN(bits)) { + return; + } + if (bitsMode === "hex") { + bitsMode = "dec"; + bitsToggle.textContent = "dec"; + bitsInput.value = String(bits); + } else { + bitsMode = "hex"; + bitsToggle.textContent = "hex"; + bitsInput.value = `0x${bits.toString(16).padStart(8, "0")}`; + } +} + +function startSolve() { + if (!validate()) { + return; + } + stopSolving(); + hashOutput.value = ""; + solveInfo.textContent = ""; + matchNote.classList.add("hidden"); + coinbaseError.textContent = ""; + coinbaseError.classList.add("hidden"); + headerError.textContent = ""; + headerError.classList.add("hidden"); + + const nonce = parseInt(nonceInput.value, 10); + const payload = { + version: parseInt(versionInput.value, 10), + prevHash: PREV_HASH, + time: parseInt(timeInput.value, 10), + bits: parseBits(), + scriptSig: getScriptSigHex(), + scriptPubKey: pkInput.value.trim(), + amount: getAmountDuffs(), + }; + + solveBtn.disabled = true; + solveBtn.textContent = "Solving..."; + nonceInput.readOnly = true; + + const t0 = performance.now(); + const threadCount = Math.max(MIN_THREADS, ((navigator.hardwareConcurrency || THREAD_DIVISOR) / THREAD_DIVISOR) | 0); + solveInfo.textContent = `Scanning nonces (${threadCount} threads)...`; + launchParallelScan(payload, nonce >>> 0, threadCount, t0); +} + +/** + * @param {object} payload + * @param {number} nonceStart + * @param {number} threadCount + * @param {number} t0 + */ +function launchParallelScan(payload, nonceStart, threadCount, t0) { + const chunkSize = Math.floor(NONCE_MAX / threadCount); + const remainder = NONCE_MAX - chunkSize * threadCount; + /** @type {number[]} */ + const perWorkerHashes = new Array(threadCount).fill(0); + /** @type {boolean[]} */ + const threadDone = new Array(threadCount).fill(false); + /** @type {{idx: number, result: object}|null} */ + let bestResult = null; + let solved = false; + + function acceptBest() { + if (solved || !bestResult) { + return; + } + clearTimeout(graceTimer); + solved = true; + const total = perWorkerHashes.reduce((a, b) => a + b, 0); + onSolved(/** @type {any} */ (bestResult.result), total, t0); + } + + function tryFinalize() { + if (solved || !bestResult) { + return; + } + for (let j = 0; j < bestResult.idx; j++) { + if (!threadDone[j]) { + return; + } + } + acceptBest(); + } + + let offset = 0; + for (let i = 0; i < threadCount; i++) { + const from = (nonceStart + offset) >>> 0; + const count = chunkSize + (i < remainder ? 1 : 0); + offset += count; + + const w = new Worker(new URL("worker.js", import.meta.url), { type: "module" }); + workers.push(w); + + const idx = i; + w.onmessage = (e) => { + if (solved) { + return; + } + const msg = e.data; + + if (msg.progress) { + perWorkerHashes[idx] = msg.totalHashes; + const total = perWorkerHashes.reduce((a, b) => a + b, 0); + const secs = (performance.now() - t0) / 1000; + const rate = secs > 0 ? ` at ${fmtRate(total / secs / 1000)}` : ""; + solveInfo.textContent = `${(total / 1e6).toFixed(1)}M hashes${rate}...`; + return; + } + + if (msg.ok) { + perWorkerHashes[idx] = msg.totalHashes; + threadDone[idx] = true; + if (!bestResult || idx < bestResult.idx) { + bestResult = { idx, result: msg.result }; + clearTimeout(graceTimer); + if (idx > 0) { + graceTimer = setTimeout(acceptBest, GRACE_TIMEOUT_MS); + } + } + tryFinalize(); + return; + } + + if (msg.done) { + perWorkerHashes[idx] = msg.totalHashes; + threadDone[idx] = true; + tryFinalize(); + if (!solved && threadDone.every(Boolean) && !bestResult) { + showError(headerError, "Nonce space exhausted"); + stopSolving(); + } + return; + } + + if (msg.error) { + solved = true; + showError(headerError, msg.error); + stopSolving(); + } + }; + + w.onerror = (e) => { + if (solved) { + return; + } + solved = true; + showError(headerError, `Worker error: ${e.message}`); + stopSolving(); + }; + + w.postMessage({ ...payload, nonceFrom: from, nonceCount: count }); + } +} + +/** + * @param {{nonce: number, hash: string, merkle_root: string}} result + * @param {number} totalHashes + * @param {number} t0 + */ +function onSolved(result, totalHashes, t0) { + const { nonce: solvedNonce, hash, merkle_root: mr } = result; + const secs = (performance.now() - t0) / 1000; + + nonceInput.value = String(solvedNonce); + merkleOutput.value = mr; + hashOutput.value = hash; + + if (totalHashes === 1) { + solveInfo.textContent = "Solved"; + } else { + const rate = secs > 0 ? ` at ${fmtRate(totalHashes / secs / 1000)}` : ""; + solveInfo.textContent = `Solved in ${secs.toFixed(1)}s (${(totalHashes / 1e6).toFixed(1)}M hashes${rate})`; + } + + const preset = currentPreset(); + if (hash === preset.hash) { + matchNote.classList.remove("hidden"); + } + + stopSolving(); +} + +function onCoinbaseChange() { + hashOutput.value = ""; + solveInfo.textContent = ""; + matchNote.classList.add("hidden"); + updateMerkleRoot(); +} + +networkSel.addEventListener("change", loadPreset); +resetBtn.addEventListener("click", loadPreset); +amountToggle.addEventListener("click", toggleAmount); +amountInput.addEventListener("input", onCoinbaseChange); +sigToggle.addEventListener("click", toggleSig); +sigInput.addEventListener("input", onCoinbaseChange); +pkInput.addEventListener("input", onCoinbaseChange); +bitsToggle.addEventListener("click", toggleBits); +versionInput.addEventListener("input", checkVersion); +solveBtn.addEventListener("click", startSolve); + +init() + .then(() => { + wasmReady = true; + loadPreset(); + }) + .catch((err) => { + showError(headerError, `Failed to load WASM module: ${err}`); + }); diff --git a/contrib/samples/solver/solver.rs b/contrib/samples/solver/solver.rs new file mode 100644 index 00000000..0231c862 --- /dev/null +++ b/contrib/samples/solver/solver.rs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Trivial scanhash implementation. + +#![no_std] + +extern crate alloc; + +use bitcoin_consensus_encoding::encode_to_vec; +use bitcoin_units::Amount; +use dash_num::{Arith256, CompactTarget}; +use dash_primitives::hash::double_sha256; +use dash_primitives::{BlockHash, BlockHeader, MerkleRoot, OutPoint, Script, Transaction, TxHash, TxIn, TxOut, TxType}; +use hex_conservative::FromHex; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use alloc::{format, vec}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +struct ScanResult { + found: bool, + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option, + #[serde(skip_serializing_if = "Option::is_none")] + hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + merkle_root: Option, + hashes: u32, +} + +/// Build a coinbase transaction. +fn build_coinbase(script_sig: Vec, script_pubkey: Vec, amount_duffs: &str) -> Result { + let sat: u64 = amount_duffs.parse().map_err(|e| format!("invalid amount: {e}"))?; + let value = Amount::from_sat(sat).map_err(|e| format!("invalid amount: {e}"))?; + Ok(Transaction { + version: 1, + tx_type: TxType::Spend, + inputs: vec![TxIn { + prevout: OutPoint { + hash: TxHash::default(), + index: 0xFFFF_FFFF, + }, + script_sig: Script::new(script_sig), + sequence: 0xFFFF_FFFF, + }], + outputs: vec![TxOut { + value, + script_pubkey: Script::new(script_pubkey), + }], + lock_time: 0, + extra_payload: Vec::new(), + }) +} + +/// Compute the merkle root from coinbase parameters. +#[wasm_bindgen] +pub fn merkle_root(script_sig_hex: &str, script_pubkey_hex: &str, amount_duffs: &str) -> Result { + let sig_bytes = Vec::::from_hex(script_sig_hex).map_err(|e| format!("invalid scriptSig hex: {e}"))?; + let pk_bytes = Vec::::from_hex(script_pubkey_hex).map_err(|e| format!("invalid scriptPubKey hex: {e}"))?; + let coinbase = build_coinbase(sig_bytes, pk_bytes, amount_duffs)?; + let tx_buf = encode_to_vec(&coinbase); + let root = MerkleRoot::from(double_sha256(&tx_buf)); + Ok(format!("{root}")) +} + +/// Scan a batch of nonces for a valid proof-of-work hash. +#[wasm_bindgen] +#[expect(clippy::too_many_arguments, reason = "flat signature required by wasm_bindgen")] +pub fn scanhash( + version: i32, + prev_hash_hex: &str, + time: u32, + bits: u32, + script_sig_hex: &str, + script_pubkey_hex: &str, + amount_duffs: &str, + nonce_start: u32, + nonce_count: u32, +) -> Result { + let prev_hash = BlockHash::from_hex(prev_hash_hex).map_err(|e| format!("invalid prev_hash hex: {e}"))?; + let sig_bytes = Vec::::from_hex(script_sig_hex).map_err(|e| format!("invalid scriptSig hex: {e}"))?; + let pk_bytes = Vec::::from_hex(script_pubkey_hex).map_err(|e| format!("invalid scriptPubKey hex: {e}"))?; + + let coinbase = build_coinbase(sig_bytes, pk_bytes, amount_duffs)?; + let tx_buf = encode_to_vec(&coinbase); + let merkle_root = MerkleRoot::from(double_sha256(&tx_buf)); + + let header = BlockHeader { + version, + prev_hash, + merkle_root, + time, + bits, + nonce: 0, + }; + let mut header_buf = encode_to_vec(&header); + + let decoded = CompactTarget(bits).decode(); + if decoded.negative || decoded.overflow { + return Err("invalid compact target".to_string()); + } + let target = decoded.value; + + let mut nonce = nonce_start; + let mut hashes: u32 = 0; + while hashes < nonce_count { + header_buf[76..80].copy_from_slice(&nonce.to_le_bytes()); + let hash = dash_pow::hash(&header_buf); + hashes += 1; + if Arith256::from(hash) <= target { + let result = ScanResult { + found: true, + nonce: Some(nonce), + hash: Some(format!("{hash}")), + merkle_root: Some(format!("{merkle_root}")), + hashes, + }; + return serde_json::to_string(&result).map_err(|e| format!("failed to serialize: {e}")); + } + nonce = nonce.wrapping_add(1); + } + + let result = ScanResult { + found: false, + nonce: None, + hash: None, + merkle_root: None, + hashes, + }; + serde_json::to_string(&result).map_err(|e| format!("failed to serialize: {e}")) +} diff --git a/contrib/samples/solver/style.css b/contrib/samples/solver/style.css new file mode 100644 index 00000000..7c7594e6 --- /dev/null +++ b/contrib/samples/solver/style.css @@ -0,0 +1,144 @@ +#wasm-genesis .hidden { + display: none; +} + +#wasm-genesis .spacer { + flex: 1; +} + +#wasm-genesis .genesis-toolbar { + display: flex; + gap: 0.5em; + align-items: center; + margin-bottom: 1em; +} + +#wasm-genesis .genesis-section { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + padding: 0.8em; + margin-bottom: 0.8em; +} + +#wasm-genesis .genesis-section-title { + font-weight: 600; + margin-bottom: 0.6em; + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--md-default-fg-color--light); +} + +#wasm-genesis .genesis-field { + margin-bottom: 0.5em; +} + +#wasm-genesis .genesis-field:last-child { + margin-bottom: 0; +} + +#wasm-genesis .genesis-field label { + display: block; + font-size: 0.85em; + font-weight: 600; + margin-bottom: 0.2em; + color: var(--md-default-fg-color); +} + +#wasm-genesis input, +#wasm-genesis textarea { + width: 100%; + font-family: 'JetBrains Mono', ui-monospace, Menlo, + Consolas, 'Liberation Mono', monospace; + font-size: 1em; + padding: 0.4em 0.6em; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + background: var(--md-code-bg-color); + color: var(--md-default-fg-color); + box-sizing: border-box; +} + +#wasm-genesis textarea { + resize: vertical; +} + +#wasm-genesis input[readonly] { + background: color-mix(in srgb, var(--md-code-bg-color) 60%, var(--md-default-fg-color--lightest)); +} + +#wasm-genesis .genesis-row { + display: flex; + gap: 0.8em; +} + +#wasm-genesis .genesis-row .genesis-field { + flex: 1; +} + +#wasm-genesis .field-with-toggle { + display: flex; + gap: 0.4em; +} + +#wasm-genesis .field-with-toggle input, +#wasm-genesis .field-with-toggle textarea { + flex: 1; + min-width: 0; +} + +#wasm-genesis .toggle-btn { + padding: 0.3em 0.6em; + font-size: 0.85em; + font-weight: 600; + white-space: nowrap; + min-width: 4em; + text-align: center; +} + +#wasm-genesis #gen-solve-info { + font-size: 0.9em; + color: var(--md-default-fg-color--light); +} + +#wasm-genesis .genesis-adm { + margin: 0.5em 0; + padding: 0.6em 0.8em; + border-radius: 4px; +} + +#wasm-genesis .genesis-warn { + border-left: 3px solid #d4a017; + background: #fef9e7; + color: #6e5b00; +} + +[data-md-color-scheme="slate"] #wasm-genesis .genesis-warn { + background: #3b3000; + color: #fcd34d; + border-left-color: #ca8a04; +} + +#wasm-genesis .genesis-note { + border-left: 3px solid #0969da; + background: #ddf4ff; + color: #0550ae; +} + +[data-md-color-scheme="slate"] #wasm-genesis .genesis-note { + background: #0d1d30; + color: #79c0ff; + border-left-color: #1f6feb; +} + +#wasm-genesis .genesis-danger { + border-left: 3px solid #cf222e; + background: #ffebe9; + color: #82071e; +} + +[data-md-color-scheme="slate"] #wasm-genesis .genesis-danger { + background: #300a0a; + color: #f85149; + border-left-color: #da3633; +} diff --git a/contrib/samples/solver/worker.js b/contrib/samples/solver/worker.js new file mode 100644 index 00000000..092d2ec8 --- /dev/null +++ b/contrib/samples/solver/worker.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2026-present, The Dash Core developers + * SPDX-License-Identifier: MIT + * See the accompanying file LICENSE or https://opensource.org/license/MIT + */ + +import init, { scanhash } from "./pkg/demo_solver.js"; + +// Nonces per scanhash call; yields between batches for progress reporting. +const BATCH_SIZE = 100000; +const ready = init(); + +self.onmessage = async (e) => { + const { version, prevHash, time, bits, scriptSig, scriptPubKey, amount, nonceFrom, nonceCount } = e.data; + let totalHashes = 0; + + try { + await ready; + let cur = nonceFrom; + let remaining = nonceCount; + while (remaining > 0) { + const count = Math.min(BATCH_SIZE, remaining); + const raw = scanhash(version, prevHash, time, bits, scriptSig, scriptPubKey, amount, cur, count); + const r = JSON.parse(raw); + totalHashes += r.hashes; + + if (r.found) { + self.postMessage({ ok: true, result: r, totalHashes }); + return; + } + + self.postMessage({ progress: true, totalHashes }); + cur = (cur + count) >>> 0; + remaining -= count; + } + self.postMessage({ done: true, totalHashes }); + } catch (err) { + self.postMessage({ ok: false, error: String(err) }); + } +}; diff --git a/contrib/zen/__init__.py b/contrib/zen/__init__.py new file mode 100644 index 00000000..665ccd25 --- /dev/null +++ b/contrib/zen/__init__.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Pre-processing used before generating Zensical documentation.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from markdown.extensions import Extension +from markdown.preprocessors import Preprocessor + +if TYPE_CHECKING: + from markdown import Markdown + +_ALERT_RE = re.compile( + r"^>[ ]?\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ ]*(.*)$" +) +_QUOTE_LINE_RE = re.compile(r"^>[ ]?(.*)$") + +_ALERT_KIND = { + "NOTE": "note", + "TIP": "tip", + "IMPORTANT": "info", + "WARNING": "warning", + "CAUTION": "danger", +} + + +class GfmAlertsPreprocessor(Preprocessor): + """Rewrite `> [!NOTE]` blocks into admonition syntax.""" + + def run(self, lines: list[str]) -> list[str]: + source = list(lines) + output: list[str] = [] + index = 0 + + while index < len(source): + line = source[index] + match = _ALERT_RE.match(line) + if match is None: + output.append(line) + index += 1 + continue + + level = _ALERT_KIND[match.group(1)] + output.append(f"!!! {level}") + + first_line = match.group(2).strip() + body: list[str] = [] + if first_line: + body.append(first_line) + + index += 1 + while index < len(source): + quoted = _QUOTE_LINE_RE.match(source[index]) + if quoted is None: + break + body.append(quoted.group(1)) + index += 1 + + if not body: + output.append(" ") + continue + + for body_line in body: + if body_line: + output.append(f" {body_line}") + else: + output.append(" ") + + return output + + +class GfmAlertsExtension(Extension): + """Markdown extension entrypoint for alert rewriting.""" + + def extendMarkdown(self, md: Markdown) -> None: + md.preprocessors.register(GfmAlertsPreprocessor(md), "zen", 110) + + +def makeExtension(**kwargs: object) -> GfmAlertsExtension: + """Construct the extension.""" + return GfmAlertsExtension(**kwargs) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..3fa0d709 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,33 @@ +## Coding Standards + +See [`guide_rust.md`](./guide_rust.md) for guidance for new and existing contributors applicable to Rust crates +in the codebase (e.g. [`pkgs/`](../pkgs/)). + +## User Guide + +The user guide is generated using [Zensical](https://pypi.org/project/zensical/) (a fork of +[MkDocs](https://pypi.org/project/mkdocs/)), configured using [`zensical.toml`](../zensical.toml) with the documentation +located in [`docs/zen`](./zen). + +### Dependencies + +Most dependencies can be installed using `python -m pip install -e '.[dev]'`. + +* Zensical (included in `[dev]`) +* PyMarkdown (included in `[dev]`) +* rjsmin (included in `[dev]`) +* [wasm-pack](https://github.com/wasm-bindgen/wasm-pack) + +### Preview + +```sh +python contrib/build_docs.py preview +``` + +### Building + +From repository root + +```sh +python contrib/build_docs.py build +``` diff --git a/docs/guide_rust.md b/docs/guide_rust.md index d11819c7..54e6f4d2 100644 --- a/docs/guide_rust.md +++ b/docs/guide_rust.md @@ -1,6 +1,6 @@ # Rust Development Guide -**Table of Contents** +## Table of Contents - [Coding Style (Rust)](#coding-style-rust) - [Formatting](#formatting) @@ -86,16 +86,23 @@ const MAX_BLOCK_SIZE: usize = 2_000_000; ### Type Safety -The type system is the first line of defence. A constraint expressed as a type is checked at compile time and costs nothing at runtime. +The type system is the first line of defence. A constraint expressed as a type is checked at compile time and +costs nothing at runtime. -- Wrap primitive types in newtypes when two values of the same underlying type carry different semantics; this prevents accidental transposition of arguments -- Prefer enums over booleans for function parameters because `Script::classify(P2pkh)` communicates intent where `Script::classify(true)` does not -- Make invalid states unrepresentable; if a combination of fields is logically impossible, restructure the type so the compiler rejects it -- Derive common traits eagerly on public types: `Clone`, `Debug`, `PartialEq`, `Eq`, `Hash`; the orphan rule prevents downstream crates from adding them later -- All public types implement `Debug` because diagnostic output and test assertions depend on it; for types holding sensitive data, provide a custom implementation that redacts the secret +- Wrap primitive types in newtypes when two values of the same underlying type carry different semantics; this prevents + accidental transposition of arguments +- Prefer enums over booleans for function parameters because `Script::classify(P2pkh)` communicates intent where + `Script::classify(true)` does not +- Make invalid states unrepresentable; if a combination of fields is logically impossible, restructure the type so the + compiler rejects it +- Derive common traits eagerly on public types: `Clone`, `Debug`, `PartialEq`, `Eq`, `Hash`; the orphan rule prevents + downstream crates from adding them later +- All public types implement `Debug` because diagnostic output and test assertions depend on it; for types holding + sensitive data, provide a custom implementation that redacts the secret > [!TIP] -> Prefer `#[derive]` for standard trait implementations. A manual implementation is warranted only when the derived behaviour would be incorrect or when redaction is needed. +> Prefer `#[derive]` for standard trait implementations. A manual implementation is warranted only when the +> derived behaviour would be incorrect or when redaction is needed.
@@ -136,13 +143,22 @@ impl core::fmt::Debug for SecretKey { ### Error Handling > [!IMPORTANT] -> Never call `.unwrap()` or `.expect()` on `Result` or `Option` in library code. A panic converts a recoverable failure into process-level failure; depending on the panic strategy, the code may unwind or abort, but either outcome is unacceptable for routine error handling. Propagate errors with `?` or handle them explicitly with `match`. Both `clippy::unwrap_used` and `clippy::expect_used` are denied at the workspace level. - -- Define domain-specific error enums with a manual `Display` implementation; gate `std::error::Error` behind the `std` feature so the error type remains usable in `no_std` contexts -- Error messages are lowercase and carry no trailing punctuation; they may be embedded in larger messages by callers, so capitalization and periods would read awkwardly in the middle of a sentence -- Keep `?` chains short; if a function contains more than a few `?` calls on unrelated operations, each operation likely belongs in its own function -- Use `#[expect]` instead of `#[allow]` when suppressing a lint; `expect` causes a warning if the suppression becomes unnecessary, preventing stale overrides from accumulating silently -- The `.todo()`, `.unimplemented()`, and `.unreachable()` family of panicking stubs are not permitted; if a branch is genuinely unreachable, restructure the types so the compiler can prove it +> Never call `.unwrap()` or `.expect()` on `Result` or `Option` in library code. A panic converts a +> recoverable failure into process-level failure; depending on the panic strategy, the code may unwind or +> abort, but either outcome is unacceptable for routine error handling. Propagate errors with `?` or handle +> them explicitly with `match`. Both `clippy::unwrap_used` and `clippy::expect_used` are denied at the +> workspace level. + +- Define domain-specific error enums with a manual `Display` implementation; gate `std::error::Error` + behind the `std` feature so the error type remains usable in `no_std` contexts +- Error messages are lowercase and carry no trailing punctuation; they may be embedded in larger messages by + callers, so capitalization and periods would read awkwardly in the middle of a sentence +- Keep `?` chains short; if a function contains more than a few `?` calls on unrelated operations, each + operation likely belongs in its own function +- Use `#[expect]` instead of `#[allow]` when suppressing a lint; `expect` causes a warning if the suppression + becomes unnecessary, preventing stale overrides from accumulating silently +- The `.todo()`, `.unimplemented()`, and `.unreachable()` family of panicking stubs are not permitted; if a + branch is genuinely unreachable, restructure the types so the compiler can prove it
@@ -213,12 +229,17 @@ fn verify_header(raw: &[u8]) -> BlockHeader { ### Ownership and Borrowing -Rust's ownership model eliminates data races and use-after-free at compile time. Working with it, rather than around it, produces code that is both safe and efficient. +Rust's ownership model eliminates data races and use-after-free at compile time. Working with it, rather than +around it, produces code that is both safe and efficient. -- Prefer borrowing over cloning; cloning allocates and copies, and signals to readers that the caller needs an independent copy -- Accept the most general reference that satisfies the function: `&str` rather than `&String`, `&[T]` rather than `&Vec`; this lets callers pass any type that dereferences to the expected slice -- Return owned values when the caller needs ownership; returning a reference to a local is a compile error, and this is intentional -- Let the caller decide when to clone; a function that silently clones its input adds hidden cost and prevents the caller from choosing a cheaper alternative +- Prefer borrowing over cloning; cloning allocates and copies, and signals to readers that the caller needs + an independent copy +- Accept the most general reference that satisfies the function: `&str` rather than `&String`, `&[T]` rather + than `&Vec`; this lets callers pass any type that dereferences to the expected slice +- Return owned values when the caller needs ownership; returning a reference to a local is a compile error, + and this is intentional +- Let the caller decide when to clone; a function that silently clones its input adds hidden cost and + prevents the caller from choosing a cheaper alternative
@@ -248,7 +269,8 @@ Consistent conversion names tell the reader the cost and ownership semantics of | `to_` | Allocates or computes | Borrows input | `Hash256::to_bytes()` | | `into_` | Variable | Consumes input | `String::into_bytes()` | -- Implement `From` for infallible conversions; the blanket impl provides `Into` automatically, so we never implement `Into` directly +- Implement `From` for infallible conversions; the blanket impl provides `Into` automatically, so we + never implement `Into` directly - Implement `TryFrom` for conversions that can fail; the associated error type documents what can go wrong - Implement `AsRef` for cheap reference-to-reference conversions @@ -292,8 +314,11 @@ impl BlockHash { ### Traits and Implementations -- Derive standard traits eagerly; the orphan rule prevents downstream crates from adding them, so we provide everything applicable up front -- Place `Serialize` and `Deserialize` behind an optional `serde` feature so crates that do not need serialization avoid the dependency; use `default-features = false` with `alloc` and `derive` features for `no_std` compatibility +- Derive standard traits eagerly; the orphan rule prevents downstream crates from adding them, so we provide + everything applicable up front +- Place `Serialize` and `Deserialize` behind an optional `serde` feature so crates that do not need + serialization avoid the dependency; use `default-features = false` with `alloc` and `derive` features for + `no_std` compatibility - Sealed traits prevent downstream implementations; this allows adding methods in future versions without a breaking change
@@ -329,9 +354,12 @@ mod private { ### Generics -- Use `impl Trait` in argument position for simple, single-use bounds; use named type parameters when the same bound appears in multiple arguments or the return type -- Prefer static dispatch for performance-critical paths; use `dyn Trait` when the set of concrete types is open or when reducing binary size matters more than call overhead -- Do not add trait bounds to struct definitions unless the bound is required for the struct's own invariants; bounds on impls are sufficient and avoid constraining downstream code +- Use `impl Trait` in argument position for simple, single-use bounds; use named type parameters when the + same bound appears in multiple arguments or the return type +- Prefer static dispatch for performance-critical paths; use `dyn Trait` when the set of concrete types is + open or when reducing binary size matters more than call overhead +- Do not add trait bounds to struct definitions unless the bound is required for the struct's own + invariants; bounds on impls are sufficient and avoid constraining downstream code
@@ -349,12 +377,15 @@ fn compute_hash(data: impl AsRef<[u8]>) -> Hash256 { ### Iterators -Iterator chains express data transformations declaratively. The compiler often optimises them into tight loops with no intermediate allocations. +Iterator chains express data transformations declaratively. The compiler often optimises them into tight loops +with no intermediate allocations. -- Implement `iter()`, `iter_mut()`, and `into_iter()` on collection types; the return types are named `Iter`, `IterMut`, and `IntoIter` respectively +- Implement `iter()`, `iter_mut()`, and `into_iter()` on collection types; the return types are named + `Iter`, `IterMut`, and `IntoIter` respectively - Implement `FromIterator` and `Extend` so the collection works with `.collect()` and `.extend()` - Prefer `filter_map()` when filtering and mapping happen together; it expresses the intent in one place -- Avoid `.collect()` when the result is only iterated once; return `impl Iterator` instead to defer allocation +- Avoid `.collect()` when the result is only iterated once; return `impl Iterator` instead to + defer allocation - Implement `size_hint()` on custom iterators so downstream consumers can pre-allocate accurately
@@ -380,7 +411,8 @@ fn spendable_outputs( ### Code Comments -Comments explain intent and context that the code alone cannot convey. Restating what the code does adds noise and drifts out of sync with the implementation. +Comments explain intent and context that the code alone cannot convey. Restating what the code does adds +noise and drifts out of sync with the implementation. #### Inline Comments @@ -391,7 +423,8 @@ Comments explain intent and context that the code alone cannot convey. Restating #### Rustdoc Comments - Documentation comments (`///`) must not exceed 80 characters wide -- The summary is at most 3 lines; do not restate the function name or signature in prose because the reader can see them directly above +- The summary is at most 3 lines; do not restate the function name or signature in prose because the reader + can see them directly above - Document `# Errors` when the function returns `Result`, listing each error variant and its cause - Document `# Panics` only when the function can panic, which should be rare - Pad lines so right columns align evenly for visual consistency @@ -451,12 +484,17 @@ fn decode_header( ### Input Validation > [!CAUTION] -> Assume every value decoded from the wire is adversarial. Trust boundaries include: consensus-encoded messages, peer-supplied byte streams, and any externally-produced data fed into a decoder. - -- **Validate early, fail loudly.** Check length, range, and structural invariants before the value reaches domain logic; return a clear error that tells the caller exactly what is wrong -- **Encode validation in types.** A newtype that can only be constructed through a validating constructor carries its proof of validity with it; downstream code never needs to re-check -- **Limit input size.** Set maximum sizes for payloads and maximum element counts for collections; unbounded input is an invitation for resource exhaustion -- **Reject non-minimal encodings.** CompactSize, VarInt, and similar variable-length encodings must use their shortest representation; accepting non-minimal forms creates consensus divergence +> Assume every value decoded from the wire is adversarial. Trust boundaries include: consensus-encoded +> messages, peer-supplied byte streams, and any externally-produced data fed into a decoder. + +- **Validate early, fail loudly.** Check length, range, and structural invariants before the value reaches + domain logic; return a clear error that tells the caller exactly what is wrong +- **Encode validation in types.** A newtype that can only be constructed through a validating constructor + carries its proof of validity with it; downstream code never needs to re-check +- **Limit input size.** Set maximum sizes for payloads and maximum element counts for collections; unbounded + input is an invitation for resource exhaustion +- **Reject non-minimal encodings.** CompactSize, VarInt, and similar variable-length encodings must use their + shortest representation; accepting non-minimal forms creates consensus divergence
@@ -499,14 +537,21 @@ impl KeyId { ### Security -- **Never log secrets.** Private keys, key shares, and seed material must never appear in log output, debug strings, or error messages -- **Implement `Debug` to redact sensitive fields.** A custom `Debug` that prints a placeholder prevents accidental exposure through `{:?}` formatting in panics -- **Use constant-time comparison for secrets.** Timing side-channels in byte-by-byte comparison leak information about secret values; use a constant-time equality function from a vetted cryptographic library -- **Zero sensitive memory after use.** Stack and heap buffers that held secrets should be zeroised before deallocation to reduce the window of exposure; the `zeroize` crate provides a `Zeroize` trait and a `ZeroizeOnDrop` derive for this purpose -- **Prefer explicit failure over silent defaults.** A default value for a missing secret silently degrades to an insecure state; failing explicitly is always safer than falling back to a placeholder +- **Never log secrets.** Private keys, key shares, and seed material must never appear in log output, debug + strings, or error messages +- **Implement `Debug` to redact sensitive fields.** A custom `Debug` that prints a placeholder prevents + accidental exposure through `{:?}` formatting in panics +- **Use constant-time comparison for secrets.** Timing side-channels in byte-by-byte comparison leak + information about secret values; use a constant-time equality function from a vetted cryptographic library +- **Zero sensitive memory after use.** Stack and heap buffers that held secrets should be zeroised before + deallocation to reduce the window of exposure; the `zeroize` crate provides a `Zeroize` trait and a + `ZeroizeOnDrop` derive for this purpose +- **Prefer explicit failure over silent defaults.** A default value for a missing secret silently degrades to + an insecure state; failing explicitly is always safer than falling back to a placeholder > [!NOTE] -> Audit dependencies regularly. A single compromised or unmaintained transitive dependency can undermine all other precautions in the codebase. +> Audit dependencies regularly. A single compromised or unmaintained transitive dependency can undermine all +> other precautions in the codebase.
diff --git a/docs/zen/index.md b/docs/zen/index.md new file mode 100644 index 00000000..7a4d9fb6 --- /dev/null +++ b/docs/zen/index.md @@ -0,0 +1,103 @@ +# Introduction + +The Base SDK for Dash is a collection of packages that enable applications to parse, construct and interact with the +Dash blockchain. This is achieved by leveraging the [`rust-bitcoin`](https://github.com/rust-bitcoin/rust-bitcoin) +ecosystem to provide a framework that builds upon existing, familiar APIs and contract expectations. + +To achieve this, state-independent portions of the consensus engine have been split into composable packages that can be +imported as needed. + +Some packages depend on other packages to expose their full functionality. Packages are primarily placed in three +buckets (see below). When upgrading downstream packages, it is recommended to version-match with the lowermost bucket +utilised by your packages. + +* Foundation packages. These packages provide unopinionated Bitcoin-compatible data manipulation. +* Base packages. These packages implement specific algorithms but without chain-distinguishing consensus logic. +* Protocol packages. These packages define the Dash protocol as deployed, blocks, transactions, chain parameters. + +```mermaid +graph LR + subgraph " " + types[dash-types] + num[dash-num] + end + subgraph " " + script[dash-script] + pow[dash-pow] + pkc[dash-pkc] + end + subgraph " " + primitives[dash-primitives] + params[dash-params] + p2p_core[dash-p2p-core] + end + + types --> num + types --> script + types --> pkc + types --> primitives + types --> p2p_core + num --> pow + num --> pkc + num --> primitives + num --> params + num --> p2p_core + script --> primitives + script --> p2p_core + pow -.-> primitives + pow -.-> params + primitives --> params + primitives --> p2p_core + params --> p2p_core +``` + +*Note: Solid lines are build dependencies, dotted lines are test dependencies.* + +## Versioning and Platform Policy + +> [!WARNING] +> +> As the Base SDK is in early development, the public contract and API shape should be considered **unstable**. Releases +> are made sporadically on an *ad hoc* basis with `YYYY-MM-DD` [tags](https://github.com/dashpay/base-sdk/tags) and +> packages have a fixed version of `0.0.0`. Stable releases will be made in accordance with +> [Semantic Versioning](https://semver.org/). + +* The minimum supported Rust version has an upper cap at the version of Rust available with Debian + [`stable`](https://wiki.debian.org/DebianStable) (last updated, [`trixie`](https://wiki.debian.org/DebianTrixie)). + The current MSRV is 1.85.0 ([source](https://github.com/dashpay/base-sdk/blob/2026-04-22/.github/workflows/build_stable.yml#L34-L35)). + +* Some features rely on Rust `nightly` (notably affected is `core::simd`, see + [rust-lang/portable-simd#364](https://github.com/rust-lang/portable-simd/issues/364)), building packages with the + `full` feature will pull in these dependencies. Consuming packages built on Rust `stable` are recommended to manually + select the features desired from the package alongside `std`. + + * Packages are validated against the version of nightly pinned in + [`rust-toolchain.toml`](https://github.com/dashpay/base-sdk/blob/2026-04-22/rust-toolchain.toml), SDK functionality + is tested only against the pinned version. Regressions on succeeding versions are evaluated on a case-by-case basis. + +* `no_std` + `alloc` is the baseline target, the public contract is limited by what is feasible by this baseline target. + `std` is primarily a passthrough to `std` features provided by dependent crates, no additional functionality is + offered by the SDK *itself* through `std` enablement. + +Crates by default offer the following features, any additional features offered by crates are defined in their +respective sections. + +| Feature | Capabilities enabled | Defined by every package | +| ----------- | ------------------------------------------- | --------------------------------- | +| _(baseline)_ | `no_std` + `alloc`, always available. | Yes | +| `std` | Standard library support. | Yes | +| `full` | All non-conflicting features. | Yes | +| `serde` | Serialization using `serde`. | **No** (dependent on feature set) | +| `_internal` | Exposes internals for tests and benchmarks. | Yes (not part of API contract) | + +## Is the SDK a full node? + +The Base SDK is not a full node implementation nor does it intend to be. `no_std` + `alloc` restricts I/O and networking +making it better suited for resource-constrained, sandboxed or otherwise limited environments. The public contract +reflects this, restricted to only stateless or relatively context-independent portions of the overall consensus engine. + +For a more batteries-included solution, consider [`rust-dashcore`](https://github.com/dashpay/rust-dashcore) or the C++ +reference implementation, [Dash Core](https://github.com/dashpay/dash) over +[RPC](https://docs.dash.org/en/stable/docs/core/api/remote-procedure-calls.html), +[REST](https://docs.dash.org/en/stable/docs/core/api/http-rest.html) or +[ZMQ](https://docs.dash.org/en/stable/docs/core/api/zmq.html). diff --git a/docs/zen/logo.svg b/docs/zen/logo.svg new file mode 100644 index 00000000..b9622bd0 --- /dev/null +++ b/docs/zen/logo.svg @@ -0,0 +1 @@ + diff --git a/docs/zen/samples/index.md b/docs/zen/samples/index.md new file mode 100644 index 00000000..ebd8bcff --- /dev/null +++ b/docs/zen/samples/index.md @@ -0,0 +1,12 @@ +# Samples + +To demonstrate the SDK's capabilities, the Base SDK comes with sample code demonstrating various protocol operations. +These samples are interactive and can be run directly within the guide. + +Native samples are `std`-dependent while web-based samples are built with [`wasm-pack`](https://wasm-bindgen.github.io/wasm-pack/installer) +targeting the `web` platform. + +| Name | Description | Web-based | +| ---- | ----------- | --------- | +| [Genesis Solver](solver/index.md) | Verify or mint a genesis block (uses `dash-pow`). | Yes | +| [Object Parser](parser/index.md) | Parse hex-encoded blocks or transactions into a navigable tree (uses `dash-primitives`). | Yes | diff --git a/docs/zen/samples/parser/index.md b/docs/zen/samples/parser/index.md new file mode 100644 index 00000000..df53aa70 --- /dev/null +++ b/docs/zen/samples/parser/index.md @@ -0,0 +1,26 @@ +# Object Parser + +Paste hex-encoded serialized data into the text area, select the object type, and click **Parse** to decode it into a +structured tree view. + +
+ + + +
+ + + +
+
+
+
+ +
+ + + + diff --git a/docs/zen/samples/solver/index.md b/docs/zen/samples/solver/index.md new file mode 100644 index 00000000..b03ab012 --- /dev/null +++ b/docs/zen/samples/solver/index.md @@ -0,0 +1,90 @@ +# Genesis Solver + +Verify a genesis block in the browser by computing the proof-of-work within the browser. Select a network to load its +genesis parameters, then click **Solve** to check whether the nonce satisfies the difficulty target. If it does not, +nonces are scanned until a valid one is found. Change the values to compute your own. + +
+ +
+ + + + + +
+ +
+
Coinbase
+ + +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+
Block header
+ +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+ +
+
Result
+ +
+ + +
+
+ + +
+
+ +
+ + + + diff --git a/docs/zen/style.css b/docs/zen/style.css new file mode 100644 index 00000000..f4390a65 --- /dev/null +++ b/docs/zen/style.css @@ -0,0 +1,24 @@ +.md-content__inner { + max-width: none; +} + +.md-typeset__table { + display: block; + width: 100%; +} + +.md-typeset table:not([class]) { + display: table; + width: 100%; + table-layout: auto; + font-size: 0.75rem; +} + +.md-typeset .admonition, +.md-typeset details { + font-size: 0.75rem; +} + +.md-typeset :not(pre) > code { + font-size: 0.94em; +} diff --git a/pkgs/primitives/Cargo.toml b/pkgs/primitives/Cargo.toml index ddcc1a8a..eff30a9a 100644 --- a/pkgs/primitives/Cargo.toml +++ b/pkgs/primitives/Cargo.toml @@ -34,7 +34,9 @@ bitcoin-units = { version = "0.3", default-features = false, features = [ dash-num = { version = "0.0.0", path = "../num" } dash-script = { version = "0.0.0", path = "../script" } dash-types = { version = "0.0.0", path = "../types", default-features = false } +cfg-if = "1" hex-conservative = { version = "0.3", default-features = false, features = ["alloc"] } +libm = { version = "0.2", default-features = false } serde = { version = "1", default-features = false, features = [ "alloc", "derive", diff --git a/pkgs/primitives/src/prelude.rs b/pkgs/primitives/src/prelude.rs index 0de6c909..af703a4c 100644 --- a/pkgs/primitives/src/prelude.rs +++ b/pkgs/primitives/src/prelude.rs @@ -10,3 +10,13 @@ pub(crate) use alloc::collections::BTreeSet; pub(crate) use alloc::format; pub(crate) use alloc::string::String; pub(crate) use alloc::vec::Vec; + +// Shim for f64::round(), see rust-lang/rust#137578. +#[cfg(feature = "serde")] +cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + pub(crate) fn round(x: f64) -> f64 { x.round() } + } else { + pub(crate) use libm::round; + } +} diff --git a/pkgs/primitives/src/serialize.rs b/pkgs/primitives/src/serialize.rs index 919f467c..05c7a694 100644 --- a/pkgs/primitives/src/serialize.rs +++ b/pkgs/primitives/src/serialize.rs @@ -11,6 +11,8 @@ /// Integers are treated as satoshis; floats are treated as whole /// coins and converted to satoshis. pub mod amount { + use crate::prelude::*; + use bitcoin_units::Amount; /// Serializes as raw satoshis. @@ -42,7 +44,7 @@ pub mod amount { if !v.is_finite() || v < 0.0 { return Err(E::custom("invalid amount")); } - let sat = (v * Amount::ONE_BTC.to_sat() as f64).round(); + let sat = round(v * Amount::ONE_BTC.to_sat() as f64); if sat > u64::MAX as f64 { return Err(E::custom("amount overflow")); } diff --git a/pyproject.toml b/pyproject.toml index 0b66a444..bbad3125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,22 @@ version = "0.0.0" requires-python = ">=3.11" [project.optional-dependencies] -dev = ["ruff>=0.9", "semgrep>=1.118.0"] +dev = [ + "Markdown>=3.7", + "pymarkdownlnt>=0.9.37", + "pygments-styles>=0.3.0", + "rjsmin>=1.2", + "ruff>=0.9", + "semgrep>=1.118.0", + "zensical>=0.0.33", +] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = [] +packages = ["contrib", "contrib.zen"] [tool.ruff] indent-width = 2 @@ -37,3 +45,12 @@ indent-style = "space" line-ending = "lf" quote-style = "double" skip-magic-trailing-comma = false + +[tool.pymarkdown.extensions.markdown-tables] +enabled = true + +[tool.pymarkdown.plugins.md013] +line_length = 120 +heading_line_length = 120 +code_blocks = false +tables = false diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 00000000..b4a51a34 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,87 @@ +[project] +site_name = "Base SDK for Dash" +site_description = "Documentation for the Base SDK for Dash." +site_author = "The Dash Core developers" +copyright = "Copyright © 2026-present, The Dash Core developers" +docs_dir = "docs/zen" +site_dir = "public" +use_directory_urls = true +repo_url = "https://github.com/dashpay/base-sdk" +repo_name = "dashpay/base-sdk" +edit_uri = "edit/develop/docs/zen" +extra_css = ["style.css"] +nav = [ + { "Home" = "index.md" }, + { "Samples" = [ + { "Samples" = "samples/index.md" }, + { "Genesis Solver" = "samples/solver/index.md" }, + { "Object Parser" = "samples/parser/index.md" }, + ] }, +] + +[project.theme] +language = "en" +logo = "logo.svg" +favicon = "logo.svg" +features = [ + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.tabs.link", + "navigation.expand", + "navigation.footer", + "navigation.indexes", + "navigation.instant.prefetch", + "navigation.instant", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", + "toc.integrate", +] + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions."contrib.zen"] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_style = "github-light-default" +pygments_lang_class = true +use_pygments = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.snippets] +base_path = ["."] +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, +] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde]