diff --git a/.cargo/config.toml.offline b/.cargo/config.toml.offline new file mode 100644 index 000000000000..f98e1b632fa7 --- /dev/null +++ b/.cargo/config.toml.offline @@ -0,0 +1,32 @@ +# Linux +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-gnu-gcc" + +[target.armv7-unknown-linux-musleabihf] +linker = "arm-linux-gnueabihf-gcc" + +[target.powerpc64le-unknown-linux-musl] +linker = "powerpc64le-linux-gnu-gcc" + +[target.riscv64gc-unknown-linux-musl] +linker = "riscv64-linux-gnu-gcc" + +[target.x86_64-unknown-linux-musl] +linker = "x86_64-linux-gnu-gcc" + +# Windows +[target.x86_64-pc-windows-gnu] +linker = "x86_64-w64-mingw32-gcc" + +# macOS +# [target.aarch64-apple-darwin] +# linker = "aarch64-apple-darwin-gcc" + +# [target.x86_64-apple-darwin] +# linker = "x86_64-apple-darwin-gcc" + +[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +# The directory for this source is set to RUST_VENDORED_SOURCES by rust/Makefile.am diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index e79c5fa4248e..9c43ca995495 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -15,6 +15,11 @@ on: description: "Short hash of the CI base image manifest for cache busting" required: true type: string + rust-vendor-artifact: + description: "Artifact holding freshly generated Rust vendor archives" + required: false + type: string + default: "" runs-on: description: "Runner label to use (e.g., ubuntu-24.04 or ubuntu-24.04-arm)" required: true @@ -141,8 +146,33 @@ jobs: uses: actions/cache/restore@v5 with: path: depends/sources - key: depends-sources-${{ hashFiles('depends/packages/*') }} - restore-keys: depends-sources- + key: depends-sources-v2-${{ hashFiles('depends/packages/*', 'depends/patches/native_cxxbridge/Cargo.lock', 'Cargo.lock') }} + restore-keys: depends-sources-v2- + + - name: Restore Rust vendor sources + id: rust-vendor-cache + if: inputs.build-target == 'linux64_rust' || inputs.build-target == 'linux64_platform_gui' + uses: actions/cache/restore@v5 + with: + path: | + depends/sources/native_cxxbridge-*-vendored.tar.gz + depends/sources/platform-cxx-*-vendored.tar.gz + depends/sources/vendored-crates-*.tar.gz + key: depends-rust-vendor-sources-${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/packages/native_cxxbridge.mk', 'depends/packages/native_rust.mk', 'depends/packages/platform_cxx.mk', 'depends/patches/native_cxxbridge/Cargo.lock', 'depends/patches/platform_cxx/cargo-config.toml', 'Cargo.lock') }} + + - name: Download Rust vendor sources + if: (inputs.build-target == 'linux64_rust' || inputs.build-target == 'linux64_platform_gui') && steps.rust-vendor-cache.outputs.cache-hit != 'true' && inputs.rust-vendor-artifact != '' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.rust-vendor-artifact }} + path: depends/sources + + - name: Check Rust vendor sources are present + if: (inputs.build-target == 'linux64_rust' || inputs.build-target == 'linux64_platform_gui') && steps.rust-vendor-cache.outputs.cache-hit != 'true' && inputs.rust-vendor-artifact == '' + run: | + echo "::error::Rust vendor source cache missed and no same-run artifact was provided" + exit 1 + shell: bash - name: Restore SDKs cache id: sdk-cache diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index 47a757d3941e..947cf0ea1372 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -29,6 +29,11 @@ on: required: false type: string default: "" + rust-vendor-artifact: + description: "Artifact holding freshly generated Rust vendor archives" + required: false + type: string + default: "" sdk-artifact: description: "Artifact holding prepared SDKs, used if the cache restore misses" required: false @@ -97,6 +102,31 @@ jobs: exit 1 shell: bash + - name: Restore Rust vendor sources + id: rust-vendor-cache + if: inputs.build-target == 'linux64_rust' + uses: actions/cache/restore@v5 + with: + path: | + depends/sources/native_cxxbridge-*-vendored.tar.gz + depends/sources/platform-cxx-*-vendored.tar.gz + depends/sources/vendored-crates-*.tar.gz + key: depends-rust-vendor-sources-${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/packages/native_cxxbridge.mk', 'depends/packages/native_rust.mk', 'depends/packages/platform_cxx.mk', 'depends/patches/native_cxxbridge/Cargo.lock', 'depends/patches/platform_cxx/cargo-config.toml', 'Cargo.lock') }} + + - name: Download Rust vendor sources + if: inputs.build-target == 'linux64_rust' && steps.rust-vendor-cache.outputs.cache-hit != 'true' && inputs.rust-vendor-artifact != '' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.rust-vendor-artifact }} + path: depends/sources + + - name: Check Rust vendor sources are present + if: inputs.build-target == 'linux64_rust' && steps.rust-vendor-cache.outputs.cache-hit != 'true' && inputs.rust-vendor-artifact == '' + run: | + echo "::error::Rust vendor source cache missed and no same-run artifact was provided" + exit 1 + shell: bash + - name: Restore depends cache id: depends-cache uses: actions/cache/restore@v5 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce8191dc99ea..8e47a7b492cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,6 +141,18 @@ jobs: base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-linux64_rust: + name: x86_64-pc-linux-gnu_rust + uses: ./.github/workflows/build-depends.yml + needs: [check-skip, container, cache-sources] + if: ${{ vars.SKIP_LINUX64_RUST == '' }} + with: + build-target: linux64_rust + container-path: ${{ needs.container.outputs.path }} + base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} + rust-vendor-artifact: ${{ needs.cache-sources.outputs.rust-vendor-artifact }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-linux64_multiprocess: name: linux64_multiprocess uses: ./.github/workflows/build-depends.yml @@ -165,6 +177,18 @@ jobs: base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-linux64_platform_gui: + name: x86_64-pc-linux-gnu_platform_gui + uses: ./.github/workflows/build-depends.yml + needs: [check-skip, container, cache-sources] + if: ${{ vars.SKIP_LINUX64_PLATFORM_GUI == '' }} + with: + build-target: linux64_platform_gui + container-path: ${{ needs.container.outputs.path }} + base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} + rust-vendor-artifact: ${{ needs.cache-sources.outputs.rust-vendor-artifact }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-mac: name: x86_64-apple-darwin uses: ./.github/workflows/build-depends.yml @@ -222,6 +246,21 @@ jobs: depends-artifact: ${{ needs.depends-linux64.outputs.built-artifact }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + src-linux64_rust: + name: linux64_rust-build + uses: ./.github/workflows/build-src.yml + needs: [check-skip, container, cache-sources, depends-linux64_rust] + if: ${{ vars.SKIP_LINUX64_RUST == '' }} + with: + build-target: linux64_rust + container-path: ${{ needs.container.outputs.path }} + depends-key: ${{ needs.depends-linux64_rust.outputs.key }} + depends-host: ${{ needs.depends-linux64_rust.outputs.host }} + depends-dep-opts: ${{ needs.depends-linux64_rust.outputs.dep-opts }} + depends-artifact: ${{ needs.depends-linux64_rust.outputs.built-artifact }} + rust-vendor-artifact: ${{ needs.cache-sources.outputs.rust-vendor-artifact }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + src-linux64_asan: name: linux64_asan-build uses: ./.github/workflows/build-src.yml @@ -277,6 +316,20 @@ jobs: depends-artifact: ${{ needs.depends-linux64_nowallet.outputs.built-artifact }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + src-linux64_platform_gui: + name: linux64_platform_gui-build + uses: ./.github/workflows/build-src.yml + needs: [check-skip, container, depends-linux64_platform_gui, lint] + if: ${{ vars.SKIP_LINUX64_PLATFORM_GUI == '' }} + with: + build-target: linux64_platform_gui + container-path: ${{ needs.container.outputs.path }} + depends-key: ${{ needs.depends-linux64_platform_gui.outputs.key }} + depends-host: ${{ needs.depends-linux64_platform_gui.outputs.host }} + depends-dep-opts: ${{ needs.depends-linux64_platform_gui.outputs.dep-opts }} + depends-artifact: ${{ needs.depends-linux64_platform_gui.outputs.built-artifact }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + src-linux64_sqlite: name: linux64_sqlite-build uses: ./.github/workflows/build-src.yml @@ -372,6 +425,17 @@ jobs: container-path: ${{ needs.container-slim.outputs.path }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + test-linux64_platform_gui: + name: linux64_platform_gui-test + uses: ./.github/workflows/test-src.yml + needs: [check-skip, container-slim, src-linux64_platform_gui, lint] + if: ${{ vars.SKIP_LINUX64_PLATFORM_GUI == '' }} + with: + bundle-key: ${{ needs.src-linux64_platform_gui.outputs.key }} + build-target: linux64_platform_gui + container-path: ${{ needs.container-slim.outputs.path }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + test-linux64_sqlite: name: linux64_sqlite-test uses: ./.github/workflows/test-src.yml diff --git a/.github/workflows/cache-depends-sources.yml b/.github/workflows/cache-depends-sources.yml index 122851d8cd72..5a261a9e73a3 100644 --- a/.github/workflows/cache-depends-sources.yml +++ b/.github/workflows/cache-depends-sources.yml @@ -8,6 +8,10 @@ on: required: false type: string default: ubuntu-24.04-arm + outputs: + rust-vendor-artifact: + description: "Artifact holding freshly generated Rust vendor archives" + value: ${{ jobs.cache-sources.outputs.rust-vendor-artifact }} schedule: # Run daily at 6 AM UTC on the default branch to keep cache warm - cron: '0 6 * * *' @@ -18,6 +22,8 @@ jobs: # Intentionally keep scheduled cache warming on GitHub-hosted ARM runners. # Blacksmith caches are expected to persist long enough without a warmup cron. runs-on: ${{ inputs.runs-on || 'ubuntu-24.04-arm' }} + outputs: + rust-vendor-artifact: ${{ steps.vendor-artifact.outputs.name }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -31,10 +37,46 @@ jobs: uses: actions/cache@v5 with: path: depends/sources - key: depends-sources-${{ hashFiles('depends/packages/*') }} - restore-keys: depends-sources- + key: depends-sources-v2-${{ hashFiles('depends/packages/*', 'depends/patches/native_cxxbridge/Cargo.lock', 'Cargo.lock') }} + restore-keys: depends-sources-v2- lookup-only: true + - name: Cache Rust vendor sources + id: rust-vendor-cache + uses: actions/cache@v5 + with: + path: | + depends/sources/native_cxxbridge-*-vendored.tar.gz + depends/sources/platform-cxx-*-vendored.tar.gz + depends/sources/vendored-crates-*.tar.gz + key: depends-rust-vendor-sources-${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/packages/native_cxxbridge.mk', 'depends/packages/native_rust.mk', 'depends/packages/platform_cxx.mk', 'depends/patches/native_cxxbridge/Cargo.lock', 'depends/patches/platform_cxx/cargo-config.toml', 'Cargo.lock') }} + - name: Download sources - if: steps.cache-check.outputs.cache-hit != 'true' - run: make -C depends download + if: | + steps.cache-check.outputs.cache-hit != 'true' || + steps.rust-vendor-cache.outputs.cache-hit != 'true' + run: | + make -C depends RUST=1 download + make -C depends PLATFORM_GUI=1 vendor-platform_cxx-crates + # The cache producer normally runs on ARM, while the Rust consumer + # runs on x86_64. Native Rust is selected from the build architecture, + # so fetch the x86_64 compiler archive explicitly as well. + make -C depends BUILD=x86_64-pc-linux-gnu RUST=1 download-one + + - name: Select Rust vendor artifact + id: vendor-artifact + if: github.event_name != 'schedule' && steps.rust-vendor-cache.outputs.cache-hit != 'true' + run: echo "name=depends-rust-vendor-sources-${{ github.run_id }}" >> "$GITHUB_OUTPUT" + + - name: Upload Rust vendor sources + if: steps.vendor-artifact.outputs.name != '' + uses: actions/upload-artifact@v6 + with: + name: ${{ steps.vendor-artifact.outputs.name }} + path: | + depends/sources/native_cxxbridge-*-vendored.tar.gz + depends/sources/platform-cxx-*-vendored.tar.gz + depends/sources/vendored-crates-*.tar.gz + compression-level: 0 + retention-days: 1 + overwrite: true diff --git a/.github/workflows/guix-build.yml b/.github/workflows/guix-build.yml index 98ecc74afe9e..d56971e75c5d 100644 --- a/.github/workflows/guix-build.yml +++ b/.github/workflows/guix-build.yml @@ -12,6 +12,17 @@ on: schedule: # Run weekly at 3 AM UTC on Sunday on the default branch - cron: '0 3 * * 0' + workflow_dispatch: + inputs: + hosts: + description: 'Host triplet to build (single host)' + required: false + default: 'x86_64-linux-gnu' + platform_gui: + description: 'Build with PLATFORM_GUI=1 (Platform GUI + vendored Rust crates)' + type: boolean + required: false + default: false jobs: build-image: @@ -19,7 +30,8 @@ jobs: if: | (github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/') || vars.RUN_GUIX_ON_ALL_PUSH == 'true')) || contains(github.event.pull_request.labels.*.name, 'guix-build') || - github.event_name == 'schedule' + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' outputs: image-tag: ${{ steps.prepare.outputs.image-tag }} repo-name: ${{ steps.prepare.outputs.repo-name }} @@ -77,7 +89,7 @@ jobs: runs-on: ubuntu-24.04-arm strategy: matrix: - build_target: [x86_64-linux-gnu, aarch64-linux-gnu, riscv64-linux-gnu, powerpc64-linux-gnu, x86_64-w64-mingw32, x86_64-apple-darwin, arm64-apple-darwin] + build_target: ${{ github.event_name == 'workflow_dispatch' && fromJSON(format('["{0}"]', inputs.hosts)) || fromJSON('["x86_64-linux-gnu", "aarch64-linux-gnu", "riscv64-linux-gnu", "powerpc64-linux-gnu", "x86_64-w64-mingw32", "x86_64-apple-darwin", "arm64-apple-darwin"]') }} timeout-minutes: 480 steps: @@ -98,7 +110,7 @@ jobs: uses: actions/cache@v5 with: path: dash/depends/sources - key: depends-sources-${{ hashFiles('dash/depends/packages/*') }} + key: depends-sources-${{ hashFiles('dash/depends/packages/*', 'dash/depends/patches/native_cxxbridge/Cargo.lock', 'dash/Cargo.lock') }} restore-keys: | depends-sources- @@ -129,7 +141,7 @@ jobs: -v ${{ github.workspace }}/.cache:/home/ubuntu/.cache \ -w /src/dash \ ghcr.io/${{ needs.build-image.outputs.repo-name }}/dashcore-guix-builder:${{ needs.build-image.outputs.image-tag }} && \ - docker exec guix-daemon bash -c 'HOSTS=${{ matrix.build_target }} /usr/local/bin/guix-start /src/dash' + docker exec guix-daemon bash -c 'HOSTS=${{ matrix.build_target }} PLATFORM_GUI=${{ inputs.platform_gui && '1' || '' }} /usr/local/bin/guix-start /src/dash' - name: Ensure build passes run: | diff --git a/.gitignore b/.gitignore index eab9f3f83ec1..4e79a440a9da 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,29 @@ compile_commands.json # Linux perf profiling artifacts perf.data perf.data.old + +# Generated by Cargo +/target/ +/rust/target/ + +# Cargo configuration +/.cargo/.configured-for-* +/.cargo/config +/.cargo/config.toml +/.cargo/dash-build.toml +/.cargo/*-linker + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# FFI bridge +/rust/*/gen/ + +src/qt/platform/moc_*.cpp diff --git a/AGENTS.md b/AGENTS.md index 6e5a58e76777..a47070702457 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,37 @@ changes, update the other in the same commit. code; reserve them for things that genuinely need explaining (non-obvious invariants, workaround rationale, non-local side effects). +## Assertions and Checks + +Full guidance lives in `doc/developer-notes.md` under "Assertions and Checks". +Short version, in order of preference: + +- `Assume(cond)` is the default. Use it for "this is how things are supposed to + be": a violation means someone has a bug worth investigating, but execution + stays well-defined. A negative rate-limit counter is the archetype - somebody + decremented twice, we may be more DoS-exposed than intended, but nothing is + corrupt. It aborts in `--enable-debug` and `--enable-fuzz` builds (CI's + `linux64_multiprocess` and fuzz jobs) while a failure is silent in release, + so it must never take down a production node. The expression is always + evaluated. +- `assert(cond)` / `Assert(cond)` is the "we must crash now" case. Use it only + when continuing would be undefined behavior, memory corruption, or corrupt + persisted/consensus state - aborting has to be the safer outcome. It should + be rare and obviously justified, but do use it where it is genuinely needed + to document and enforce a precondition that keeps the code below it safe. + `Assert` returns its argument: `assert(ptr != nullptr); obj = *ptr;` becomes + `obj = *Assert(ptr);` +- `CHECK_NONFATAL(cond)` / `NONFATAL_UNREACHABLE()` for internal logic bugs on + a path with a caller to report to. Required in RPC code, enforced + (best-effort) by `test/lint/lint-assertions.py` for `src/rpc/` and + `src/wallet/rpc*`. + +None of these validate input. Data from peers, RPC arguments, wallet files, or +on-disk state must be checked and rejected through normal error handling - +asserting on it turns a peer-triggered inconsistency into a remote crash. +Environment failures (disk full, corrupt block on disk, failed DB write) are +not checks at all: return an error, `AbortNode()`, or `InitError()`. + ## Repository Map - `src/` - C++ implementation. diff --git a/CLAUDE.md b/CLAUDE.md index 6e5a58e76777..a47070702457 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,37 @@ changes, update the other in the same commit. code; reserve them for things that genuinely need explaining (non-obvious invariants, workaround rationale, non-local side effects). +## Assertions and Checks + +Full guidance lives in `doc/developer-notes.md` under "Assertions and Checks". +Short version, in order of preference: + +- `Assume(cond)` is the default. Use it for "this is how things are supposed to + be": a violation means someone has a bug worth investigating, but execution + stays well-defined. A negative rate-limit counter is the archetype - somebody + decremented twice, we may be more DoS-exposed than intended, but nothing is + corrupt. It aborts in `--enable-debug` and `--enable-fuzz` builds (CI's + `linux64_multiprocess` and fuzz jobs) while a failure is silent in release, + so it must never take down a production node. The expression is always + evaluated. +- `assert(cond)` / `Assert(cond)` is the "we must crash now" case. Use it only + when continuing would be undefined behavior, memory corruption, or corrupt + persisted/consensus state - aborting has to be the safer outcome. It should + be rare and obviously justified, but do use it where it is genuinely needed + to document and enforce a precondition that keeps the code below it safe. + `Assert` returns its argument: `assert(ptr != nullptr); obj = *ptr;` becomes + `obj = *Assert(ptr);` +- `CHECK_NONFATAL(cond)` / `NONFATAL_UNREACHABLE()` for internal logic bugs on + a path with a caller to report to. Required in RPC code, enforced + (best-effort) by `test/lint/lint-assertions.py` for `src/rpc/` and + `src/wallet/rpc*`. + +None of these validate input. Data from peers, RPC arguments, wallet files, or +on-disk state must be checked and rejected through normal error handling - +asserting on it turns a peer-triggered inconsistency into a remote crash. +Environment failures (disk full, corrupt block on disk, failed DB write) are +not checks at all: return an error, `AbortNode()`, or `InitError()`. + ## Repository Map - `src/` - C++ implementation. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000000..993388d734bc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,304 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "chirp" +version = "0.1.0" +dependencies = [ + "built", + "cxx", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dashrust" +version = "0.1.0" +dependencies = [ + "chirp", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000000..e3bba0d89d4a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = [ + "rust/chirp", + "rust/dashrust", +] +resolver = "2" + +[profile.release] +codegen-units = 1 +lto = true +panic = "unwind" + +[profile.dev] +codegen-units = 1 +panic = "unwind" diff --git a/Makefile.am b/Makefile.am index ea2cba9fd2fa..6c77fdd262ab 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,10 +8,15 @@ print-%: FORCE @echo '$*'='$($*)' ACLOCAL_AMFLAGS = -I build-aux/m4 -SUBDIRS = src +SUBDIRS = +if ENABLE_RUST +SUBDIRS += rust +endif +SUBDIRS += src if ENABLE_MAN SUBDIRS += doc/man endif +DIST_SUBDIRS = rust src doc/man .PHONY: deploy FORCE .INTERMEDIATE: $(COVERAGE_INFO) @@ -65,6 +70,13 @@ WINDOWS_PACKAGING = $(top_srcdir)/share/pixmaps/dash.ico \ OSX_PACKAGING = $(OSX_DEPLOY_SCRIPT) $(OSX_INSTALLER_ICONS) \ $(top_srcdir)/contrib/macdeploy/detached-sig-create.sh +CARGO_FILES = \ + $(top_srcdir)/.cargo/config.toml.offline \ + $(top_srcdir)/Cargo.lock \ + $(top_srcdir)/Cargo.toml \ + $(top_srcdir)/rust-toolchain.toml \ + $(top_srcdir)/contrib/devtools/cargo-vendor-git-sources.sh + COVERAGE_INFO = $(COV_TOOL_WRAPPER) baseline.info \ test_dash_filtered.info total_coverage.info \ baseline_filtered.info functional_test.info functional_test_filtered.info \ @@ -249,7 +261,7 @@ endif dist_noinst_SCRIPTS = autogen.sh -EXTRA_DIST = $(DIST_SHARE) $(DIST_CONTRIB) $(WINDOWS_PACKAGING) $(OSX_PACKAGING) $(BIN_CHECKS) +EXTRA_DIST = $(DIST_SHARE) $(DIST_CONTRIB) $(WINDOWS_PACKAGING) $(OSX_PACKAGING) $(BIN_CHECKS) $(CARGO_FILES) EXTRA_DIST += \ test/functional \ diff --git a/ci/dash/lint-cstyle-casts.py b/ci/dash/lint-cstyle-casts.py new file mode 100755 index 000000000000..eba2d0617d1f --- /dev/null +++ b/ci/dash/lint-cstyle-casts.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Support C-style cast linting in Dash-specific C++ code.""" + +import argparse +import json +import re +import shlex +import subprocess +import sys +from pathlib import Path +from typing import TextIO + + +CPP_SOURCE_EXTENSIONS = {".cc", ".cpp", ".cxx"} +DIAGNOSTIC_RE = re.compile(r"^(.*?):\d+:\d+: (?:warning|error): .*") +MACRO_EXPANSION_RE = re.compile(r"^(.*?):\d+:\d+: note: expanded from macro .*") +OLD_STYLE_CAST_DIAGNOSTICS = {"clang-diagnostic-old-style-cast", "google-readability-casting"} + + +def get_dash_files(source_root: Path) -> list[str]: + manifest = source_root / "test/util/data/non-backported.txt" + patterns = [line.strip() for line in manifest.read_text(encoding="utf8").splitlines() if line.strip()] + result = subprocess.run( + ["git", "ls-files", "--", *patterns], + cwd=source_root, + check=True, + stdout=subprocess.PIPE, + text=True, + encoding="utf8", + ) + return [line for line in result.stdout.splitlines() if line] + + +def is_dash_file(path: str, dash_files: set[str]) -> bool: + normalized = path.replace("\\", "/") + return any(normalized == dash_file or normalized.endswith(f"/{dash_file}") for dash_file in dash_files) + + +def prepare_compile_database(source_root: Path, input_path: Path, output_dir: Path) -> None: + database = json.loads(input_path.read_text(encoding="utf8")) + + for entry in database: + if "arguments" not in entry: + entry["arguments"] = shlex.split(entry.pop("command")) + entry["arguments"].extend(["-Wold-style-cast", "-Wno-error=old-style-cast"]) + + output_dir.mkdir(parents=True, exist_ok=True) + (output_dir / "compile_commands.json").write_text(json.dumps(database), encoding="utf8") + + +def filter_diagnostics(source_root: Path, input_stream: TextIO, output_stream: TextIO) -> bool: + dash_files = set(get_dash_files(source_root)) + group: list[str] = [] + found_violation = False + + def flush() -> None: + nonlocal found_violation + if not group: + return + diagnostic_match = DIAGNOSTIC_RE.match(group[0]) + is_cast_diagnostic = any(diag in group[0] for diag in OLD_STYLE_CAST_DIAGNOSTICS) + macro_expansions = [ + expansion_match + for line in group + if (expansion_match := MACRO_EXPANSION_RE.match(line)) + ] + target_file = macro_expansions[-1].group(1) if macro_expansions else (diagnostic_match.group(1) if diagnostic_match else "") + is_dash_diagnostic = is_dash_file(target_file, dash_files) if target_file else False + + if not is_cast_diagnostic or is_dash_diagnostic: + output_stream.writelines(group) + found_violation |= is_cast_diagnostic and is_dash_diagnostic + group.clear() + + for line in input_stream: + if DIAGNOSTIC_RE.match(line): + flush() + group.append(line) + elif group: + group.append(line) + else: + output_stream.write(line) + flush() + return found_violation + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + prepare = subparsers.add_parser("prepare", help="create a Dash-aware compilation database") + prepare.add_argument("--input", type=Path, required=True) + prepare.add_argument("--output-dir", type=Path, required=True) + prepare.add_argument("--source-root", type=Path, required=True) + + filter_parser = subparsers.add_parser("filter", help="filter clang-tidy diagnostics") + filter_parser.add_argument("--source-root", type=Path, required=True) + + args = parser.parse_args() + source_root = args.source_root.resolve() + if args.command == "prepare": + prepare_compile_database(source_root, args.input.resolve(), args.output_dir.resolve()) + return 0 + return int(filter_diagnostics(source_root, sys.stdin, sys.stdout)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ci/dash/lint-tidy.sh b/ci/dash/lint-tidy.sh index 757009610a9c..e1d736aed287 100755 --- a/ci/dash/lint-tidy.sh +++ b/ci/dash/lint-tidy.sh @@ -66,8 +66,20 @@ python3 "${CLANG_TIDY_CACHE_PY}" --zero-stats 2>&1 || true cd "${BASE_ROOT_DIR}/build-ci/dashcore-${BUILD_TARGET}/src" -if ! ( run-clang-tidy -clang-tidy-binary="${CLANG_TIDY_CACHE}" -quiet "${MAKEJOBS}" | tee tmp.tidy-out.txt ); then - grep -C5 "error: " tmp.tidy-out.txt +CAST_LINT_DB="${PWD}/../cstyle-cast-compile-db" +python3 "${BASE_ROOT_DIR}/ci/dash/lint-cstyle-casts.py" prepare \ + --input "${PWD}/../compile_commands.json" \ + --output-dir "${CAST_LINT_DB}" \ + --source-root "${BASE_ROOT_DIR}" + +if ! ( run-clang-tidy \ + -checks=clang-diagnostic-old-style-cast,google-readability-casting \ + -clang-tidy-binary="${CLANG_TIDY_CACHE}" \ + -p "${CAST_LINT_DB}" \ + -quiet "${MAKEJOBS}" | \ + python3 "${BASE_ROOT_DIR}/ci/dash/lint-cstyle-casts.py" filter --source-root "${BASE_ROOT_DIR}" | \ + tee tmp.tidy-out.txt ); then + grep -E -C5 "error: |warning: use of old-style cast|google-readability-casting" tmp.tidy-out.txt echo "^^^ ⚠️ Failure generated from clang-tidy" false fi diff --git a/ci/dash/matrix.sh b/ci/dash/matrix.sh index 49117d71c82f..feefdfff9deb 100755 --- a/ci/dash/matrix.sh +++ b/ci/dash/matrix.sh @@ -12,7 +12,7 @@ source ./ci/test/00_setup_env.sh # Configure sanitizers options export ASAN_OPTIONS="detect_leaks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1" -export LSAN_OPTIONS="suppressions=${BASE_ROOT_DIR}/test/sanitizer_suppressions/lsan" +export LSAN_OPTIONS="suppressions=${BASE_ROOT_DIR}/test/sanitizer_suppressions/lsan:print_suppressions=0" export TSAN_OPTIONS="suppressions=${BASE_ROOT_DIR}/test/sanitizer_suppressions/tsan:halt_on_error=1:second_deadlock_stack=1" export UBSAN_OPTIONS="suppressions=${BASE_ROOT_DIR}/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1" @@ -20,6 +20,8 @@ if [ "$BUILD_TARGET" = "aarch64-linux" ]; then source ./ci/test/00_setup_env_aarch64.sh elif [ "$BUILD_TARGET" = "linux64" ]; then source ./ci/test/00_setup_env_native_qt5.sh +elif [ "$BUILD_TARGET" = "linux64_rust" ]; then + source ./ci/test/00_setup_env_native_rust.sh elif [ "$BUILD_TARGET" = "linux64_asan" ]; then source ./ci/test/00_setup_env_native_asan.sh elif [ "$BUILD_TARGET" = "linux64_fuzz" ]; then @@ -28,6 +30,8 @@ elif [ "$BUILD_TARGET" = "linux64_multiprocess" ]; then source ./ci/test/00_setup_env_native_multiprocess.sh elif [ "$BUILD_TARGET" = "linux64_nowallet" ]; then source ./ci/test/00_setup_env_native_nowallet_libbitcoinkernel.sh +elif [ "$BUILD_TARGET" = "linux64_platform_gui" ]; then + source ./ci/test/00_setup_env_native_platform_gui.sh elif [ "$BUILD_TARGET" = "linux64_sqlite" ]; then source ./ci/test/00_setup_env_native_sqlite.sh elif [ "$BUILD_TARGET" = "linux64_tsan" ]; then diff --git a/ci/test/00_setup_env_native_asan.sh b/ci/test/00_setup_env_native_asan.sh index c3029cfe68b4..909c42ece182 100755 --- a/ci/test/00_setup_env_native_asan.sh +++ b/ci/test/00_setup_env_native_asan.sh @@ -7,12 +7,12 @@ export LC_ALL=C.UTF-8 export CONTAINER_NAME=ci_native_asan -export PACKAGES="clang-19 llvm-19 libclang-rt-19-dev python3-zmq qtbase5-dev qttools5-dev-tools libevent-dev bsdmainutils libboost-dev libdb5.3++-dev libminiupnpc-dev libzmq3-dev libqrencode-dev" +export PACKAGES="clang-19 llvm-19 libclang-rt-19-dev python3-zmq qtbase5-dev qttools5-dev-tools libevent-dev bsdmainutils libboost-dev libminiupnpc-dev libzmq3-dev libqrencode-dev" # Reuses the depends built for the linux64 target, which uses the defaults. export DEP_OPTS="" export TEST_RUNNER_EXTRA="--timeout-factor=4 -j2" # Increase timeout because sanitizers slow down export GOAL="install" -export BITCOIN_CONFIG="--enable-zmq --enable-crash-hooks --with-gui=qt5 \ +export BITCOIN_CONFIG="--enable-zmq --enable-crash-hooks --with-gui=qt5 --without-bdb --with-sqlite \ --with-sanitizers=address,float-divide-by-zero,integer,undefined \ CPPFLAGS='-DARENA_DEBUG -DDEBUG_LOCKORDER' \ CC='clang-19 -ftrivial-auto-var-init=pattern' CXX='clang++-19 -ftrivial-auto-var-init=pattern'" diff --git a/ci/test/00_setup_env_native_platform_gui.sh b/ci/test/00_setup_env_native_platform_gui.sh new file mode 100755 index 000000000000..1f8251038b2d --- /dev/null +++ b/ci/test/00_setup_env_native_platform_gui.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C.UTF-8 + +# Builds dash-qt with the optional Dash Platform GUI (--enable-platform-gui) +# turned on and runs the platform_* unit-test suites. PLATFORM_GUI=1 makes +# depends build mbedtls and the Platform-owned CXX binding archive; proof +# verification, DPP decoding and state-transition construction come from that +# archive. Functional tests are skipped: the feature is exercised by +# the gated C++ unit tests (platform_drive_tests, platform_dpp_tests, +# platformkeys_tests) and there is no dashd-only surface to drive. +export CONTAINER_NAME=ci_native_platform_gui +export HOST=x86_64-pc-linux-gnu +export PACKAGES="python3-zmq qtbase5-dev qttools5-dev-tools libdbus-1-dev libharfbuzz-dev" +export DEP_OPTS="PLATFORM_GUI=1" +export RUN_UNIT_TESTS="true" +export RUN_UNIT_TESTS_SEQUENTIAL="false" +export RUN_FUNCTIONAL_TESTS="false" +export GOAL="install" +export BITCOIN_CONFIG="--enable-platform-gui --with-gui=qt5 --enable-zmq --with-libs=no --enable-reduce-exports LDFLAGS=-static-libstdc++" diff --git a/ci/test/00_setup_env_native_rust.sh b/ci/test/00_setup_env_native_rust.sh new file mode 100755 index 000000000000..fe12acd24db9 --- /dev/null +++ b/ci/test/00_setup_env_native_rust.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C.UTF-8 + +export CONTAINER_NAME=ci_native_rust +export HOST=x86_64-pc-linux-gnu +export DEP_OPTS="RUST=1 NO_QT=1 NO_QR=1" +export RUN_FUNCTIONAL_TESTS="false" +export GOAL="install" +export BITCOIN_CONFIG="--enable-rust --with-gui=no --enable-reduce-exports" diff --git a/ci/test/04_install.sh b/ci/test/04_install.sh index dbe478598b11..992d1ed19b17 100755 --- a/ci/test/04_install.sh +++ b/ci/test/04_install.sh @@ -15,7 +15,7 @@ mkdir -p "${CCACHE_DIR}" mkdir -p "${PREVIOUS_RELEASES_DIR}" export ASAN_OPTIONS="detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1" -export LSAN_OPTIONS="suppressions=${BASE_BUILD_DIR}/test/sanitizer_suppressions/lsan" +export LSAN_OPTIONS="suppressions=${BASE_BUILD_DIR}/test/sanitizer_suppressions/lsan:print_suppressions=0" export TSAN_OPTIONS="suppressions=${BASE_BUILD_DIR}/test/sanitizer_suppressions/tsan:halt_on_error=1" export UBSAN_OPTIONS="suppressions=${BASE_BUILD_DIR}/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1" env | grep -E '^(BASE_|QEMU_|CCACHE_|LC_ALL|BOOST_TEST_RANDOM|DEBIAN_FRONTEND|CONFIG_SHELL|(ASAN|LSAN|TSAN|UBSAN)_OPTIONS|PREVIOUS_RELEASES_DIR))' | tee /tmp/env diff --git a/configure.ac b/configure.ac index d36f8046cfa2..a3279e8cb0cf 100644 --- a/configure.ac +++ b/configure.ac @@ -48,6 +48,7 @@ if test "${ARFLAGS+set}" != "set"; then ARFLAGS="cr" fi +AC_CANONICAL_BUILD AC_CANONICAL_HOST AH_TOP([#ifndef DASH_CONFIG_H]) @@ -60,7 +61,9 @@ AM_INIT_AUTOMAKE([1.13 no-define subdir-objects foreign]) AM_MAINTAINER_MODE([enable]) dnl make the compilation flags quiet unless V=1 is used -AM_SILENT_RULES([yes]) +m4_pattern_allow([AM_DEFAULT_VERBOSITY]) +m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])],[AM_DEFAULT_VERBOSITY=1]) +AM_CONDITIONAL([SILENT_RULES], [test "$AM_DEFAULT_VERBOSITY" = '0']) dnl Compiler checks (here before libtool). if test "${CXXFLAGS+set}" = "set"; then @@ -125,6 +128,42 @@ AC_PATH_TOOL([DSYMUTIL], [dsymutil]) AC_PATH_PROG([DOXYGEN], [doxygen]) AM_CONDITIONAL([HAVE_DOXYGEN], [test -n "$DOXYGEN"]) +dnl Rust toolchain detection. These programs are required only when a +dnl Rust-dependent component is explicitly enabled. +AC_PATH_PROG([CARGO], [cargo]) +AC_PATH_PROG([CXXBRIDGE], [cxxbridge]) +AC_PATH_PROG([RUSTC], [rustc]) +CXXBRIDGE_REQUIRED_VERSION="1.0.198" +RUSTC_REQUIRED_VERSION="1.92.0" +CARGO_REQUIRED_VERSION="$RUSTC_REQUIRED_VERSION" +AC_SUBST([CXXBRIDGE_REQUIRED_VERSION]) + +AC_ARG_VAR([CARGO_INCREMENTAL], [Enable incremental compilation for Rust]) +if test "${CARGO_INCREMENTAL+set}" != "set"; then + CARGO_INCREMENTAL="0" +fi + +AC_ARG_VAR([RUSTFLAGS], [Flags for Rust compiler]) + +if test "${RUSTFLAGS+set}" != "set"; then + RUSTFLAGS="-C embed-bitcode=yes -C relocation-model=pic" +fi + +AC_ARG_VAR([RUST_VENDORED_SOURCES], [Directory containing vendored Rust crate sources for offline builds]) + +AC_ARG_ENABLE([rust], + [AS_HELP_STRING([--enable-rust], + [build the Rust/C++ bridge smoke component (default is no)])], + [enable_rust=$enableval], + [enable_rust=no]) + +AC_ARG_ENABLE([online-rust], + [AS_HELP_STRING([--enable-online-rust], + [allow Cargo to fetch Rust dependencies from the internet (default is no, using vendored dependencies offline)])], + [enable_online_rust=$enableval], + [enable_online_rust=no]) +AM_CONDITIONAL([ENABLE_ONLINE_RUST], [test "$enable_online_rust" != "no"]) + AC_ARG_ENABLE([wallet], [AS_HELP_STRING([--disable-wallet], [disable wallet (enabled by default)])], @@ -275,6 +314,7 @@ AC_ARG_ENABLE([debug], [use compiler flags and macros suited for debugging (default is no)])], [enable_debug=$enableval], [enable_debug=no]) +AM_CONDITIONAL([ENABLE_DEBUG], [test "$enable_debug" = "yes"]) dnl Enable exception stacktraces AC_ARG_ENABLE([stacktraces], @@ -301,6 +341,24 @@ if test "$enable_miner" = "yes"; then AC_DEFINE(ENABLE_MINER, 1, [Define this symbol if in-wallet miner should be enabled]) fi +dnl Enable Dash Platform support (usernames / DashPay contacts) in the GUI. +dnl This only affects dash-qt; dashd and the other binaries never link any of it. +AC_ARG_ENABLE([platform-gui], + [AS_HELP_STRING([--enable-platform-gui], + [enable Dash Platform (usernames/DashPay) support in the GUI (default is no)])], + [enable_platform_gui=$enableval], + [enable_platform_gui=no]) +AC_ARG_VAR([MBEDTLS_CFLAGS], [C compiler flags for mbedtls, bypasses autodetection]) +AC_ARG_VAR([MBEDTLS_LIBS], [Linker flags for mbedtls, bypasses autodetection]) +AC_ARG_VAR([PLATFORM_CXX_CFLAGS], [C++ compiler flags for the Dash Platform CXX bindings]) +AC_ARG_VAR([PLATFORM_CXX_LIBS], [Linker flags for the Dash Platform CXX bindings]) +dnl Rust static libraries each contain their language runtime. Linking Core's +dnl smoke archive and Platform's independently built archive into one binary +dnl would duplicate Rust and CXX runtime symbols. +if test "$enable_platform_gui" = "yes" && test "$enable_rust" = "yes"; then + AC_MSG_ERROR([--enable-platform-gui cannot be combined with --enable-rust]) +fi + dnl Enable different -fsanitize options AC_ARG_WITH([sanitizers], [AS_HELP_STRING([--with-sanitizers], @@ -881,6 +939,16 @@ case $host in export PKG_CONFIG_PATH="$($BREW --prefix qt@5 2>/dev/null)/lib/pkgconfig:$PKG_CONFIG_PATH" fi + if test "$enable_platform_gui" = "yes" && $BREW list --versions mbedtls >/dev/null && test "$MBEDTLS_CFLAGS" = "" && test "$MBEDTLS_LIBS" = ""; then + mbedtls_prefix=$($BREW --prefix mbedtls 2>/dev/null) + if test "$suppress_external_warnings" != "no"; then + MBEDTLS_CFLAGS="-isystem $mbedtls_prefix/include" + else + MBEDTLS_CFLAGS="-I$mbedtls_prefix/include" + fi + MBEDTLS_LIBS="-L$mbedtls_prefix/lib -lmbedtls -lmbedx509 -lmbedcrypto" + fi + gmp_prefix=$($BREW --prefix gmp 2>/dev/null) if test "$gmp_prefix" != ""; then if test "$suppress_external_warnings" != "no"; then @@ -1694,6 +1762,193 @@ if test "$use_zmq" = "yes"; then esac fi +dnl Rust support is deliberately confined to the hosts we validate (the Guix +dnl release set plus native development hosts). Any other host resolves to an +dnl empty triple, so --enable-rust fails explicitly rather than emitting +dnl binaries for a target we never build or test. +AC_DEFUN([RS_SET_TRIPLE], [ + rs_triple_libc=$3 + case $2 in + *-linux-android*) dnl unsupported; must stay above the generic linux arms + $1="" ;; + aarch64-*-linux*|arm64-*-linux*) + $1="aarch64-unknown-linux-${rs_triple_libc}" ;; + riscv64-*-linux*) $1="riscv64gc-unknown-linux-${rs_triple_libc}" ;; + x86_64-*-linux*) $1="x86_64-unknown-linux-${rs_triple_libc}" ;; + x86_64-*-darwin*) $1="x86_64-apple-darwin" ;; + aarch64-*-darwin*|arm64-*-darwin*) + $1="aarch64-apple-darwin" ;; + x86_64-*-mingw*) $1="x86_64-pc-windows-gnu" ;; + *) $1="" ;; + esac +]) + +RUST_LIBS="" +RUST_MACOS_DEPLOYMENT_TARGET="" +RUST_OSX_SDK="" +RUST_TARGET="" +RUST_NATIVE="" + +if test "$enable_rust" = "yes"; then + if test -z "$CARGO" || test -z "$RUSTC" || test -z "$CXXBRIDGE"; then + AC_MSG_ERROR([cargo, rustc and cxxbridge are required by --enable-rust]) + fi + AC_MSG_CHECKING([for cxxbridge $CXXBRIDGE_REQUIRED_VERSION]) + cxxbridge_version=`$CXXBRIDGE --version 2>/dev/null` + if test "$cxxbridge_version" != "cxxbridge $CXXBRIDGE_REQUIRED_VERSION"; then + AC_MSG_RESULT([no ($cxxbridge_version)]) + AC_MSG_ERROR([cxxbridge $CXXBRIDGE_REQUIRED_VERSION is required by --enable-rust]) + fi + AC_MSG_RESULT([yes]) + AC_MSG_CHECKING([for cargo $CARGO_REQUIRED_VERSION]) + cargo_version=`$CARGO --version 2>/dev/null | cut -d ' ' -f 2` + if test "$cargo_version" != "$CARGO_REQUIRED_VERSION"; then + AC_MSG_RESULT([no ($cargo_version)]) + AC_MSG_ERROR([cargo $CARGO_REQUIRED_VERSION is required by --enable-rust]) + fi + AC_MSG_RESULT([yes]) + AC_MSG_CHECKING([for rustc $RUSTC_REQUIRED_VERSION]) + rustc_version=`$RUSTC --version 2>/dev/null | cut -d ' ' -f 2` + if test "$rustc_version" != "$RUSTC_REQUIRED_VERSION"; then + AC_MSG_RESULT([no ($rustc_version)]) + AC_MSG_ERROR([rustc $RUSTC_REQUIRED_VERSION is required by --enable-rust]) + fi + AC_MSG_RESULT([yes]) + rust_uses_depends=no + if test -n "$depends_prefix" && test "$RUSTC" = "$depends_prefix/native/bin/rustc"; then + rust_uses_depends=yes + fi + if test "$enable_online_rust" = "no"; then + if test -z "$RUST_VENDORED_SOURCES"; then + AC_MSG_ERROR([--enable-rust requires RUST_VENDORED_SOURCES for offline builds or --enable-online-rust]) + fi + if test ! -d "$RUST_VENDORED_SOURCES"; then + AC_MSG_ERROR([Rust vendored sources directory does not exist: $RUST_VENDORED_SOURCES]) + fi + fi + AC_DEFINE([ENABLE_RUST], [1], [Define this symbol to enable Rust components]) + + case $host in + *mingw*) + RUST_LIBS="$RUST_LIBS -luserenv -lntdll" + ;; + *linux*) + RUST_LIBS="$RUST_LIBS -ldl" + ;; + esac + + case $host in + *darwin*) + AC_MSG_CHECKING([for target macOS version]) + if test -z "$OSX_MIN_VERSION" && test "$cross_compiling" = "yes"; then + AC_MSG_ERROR([Cross-compilation requires OSX_MIN_VERSION to be set, cannot continue!]) + fi + if test -n "$OSX_MIN_VERSION"; then + RUST_MACOS_DEPLOYMENT_TARGET="$OSX_MIN_VERSION" + elif test -n "$MACOSX_DEPLOYMENT_TARGET"; then + RUST_MACOS_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" + else + dnl Match the C/C++ compiler's effective deployment target so Rust + dnl objects never claim a different minimum macOS version than the + dnl C++ objects they are linked with. + rust_macos_min_raw=`echo __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ | $CXX $CXXFLAGS -E -xc++ - 2>/dev/null | tail -n 1` + case $rust_macos_min_raw in + [[0-9]][[0-9]]*) + RUST_MACOS_DEPLOYMENT_TARGET="`expr $rust_macos_min_raw / 10000`.`expr $rust_macos_min_raw % 10000 / 100`" + ;; + *) + RUST_MACOS_DEPLOYMENT_TARGET=`sw_vers -productVersion` + ;; + esac + fi + AC_MSG_RESULT([$RUST_MACOS_DEPLOYMENT_TARGET]) + ;; + esac + + AC_MSG_CHECKING([for Rust target]) + rust_target_libc=gnu + case $host in + *-linux-musl*) rust_target_libc=musl ;; + esac + if test "$rust_uses_depends" = "yes"; then + rust_target_libc=musl + fi + RS_SET_TRIPLE([RUST_TARGET], [$host], [$rust_target_libc]) + if test "$RUST_TARGET" = ""; then + AC_MSG_RESULT([unknown]) + AC_MSG_ERROR([Unsupported target for Rust: $host]) + fi + AC_MSG_RESULT([$RUST_TARGET]) + + AC_MSG_CHECKING([for Rust target standard library]) + rust_target_libdir=`$RUSTC --print target-libdir --target "$RUST_TARGET" 2>/dev/null` + if test -z "$rust_target_libdir" || test ! -d "$rust_target_libdir"; then + AC_MSG_RESULT([not found]) + AC_MSG_ERROR([Rust standard library for $RUST_TARGET is not installed]) + fi + AC_MSG_RESULT([$rust_target_libdir]) + + case $host in + *darwin*) + AC_MSG_CHECKING([for Xcode SDK]) + if test -z "$OSX_SDK" && test "$cross_compiling" = "yes"; then + AC_MSG_ERROR([Cross-compilation requires OSX_SDK to be set to path of Xcode SDK, cannot continue!]) + fi + RUST_OSX_SDK="${OSX_SDK:-$(xcrun --sdk macosx --show-sdk-path)}" + AC_MSG_RESULT([$RUST_OSX_SDK]) + ;; + esac + + AC_MSG_CHECKING([for Rust host]) + rust_native_libc=gnu + if test "$rust_uses_depends" = "no"; then + case $build in + *-linux-musl*) rust_native_libc=musl ;; + esac + fi + RS_SET_TRIPLE([RUST_NATIVE], [$build], [$rust_native_libc]) + if test "$RUST_NATIVE" = ""; then + if test "$cross_compiling" = "yes"; then + AC_MSG_RESULT([unknown]) + AC_MSG_ERROR([Unsupported host for Rust: $build]) + fi + RUST_NATIVE="$RUST_TARGET" + fi + AC_MSG_RESULT([$RUST_NATIVE]) + + AC_MSG_CHECKING([for host archiver]) + if test -z "$NATIVE_AR"; then + if test "$cross_compiling" = "yes"; then + AC_MSG_ERROR([Cross-compilation requires NATIVE_AR to be set to host archiver, cannot continue!]) + fi + NATIVE_AR="$AR" + fi + AC_MSG_RESULT([$NATIVE_AR]) + + AC_MSG_CHECKING([for host C compiler]) + if test -z "$NATIVE_CC"; then + if test "$cross_compiling" = "yes"; then + AC_MSG_ERROR([Cross-compilation requires NATIVE_CC to be set to host C compiler, cannot continue!]) + fi + NATIVE_CC="$CC" + fi + AC_MSG_RESULT([$NATIVE_CC]) + + AC_MSG_CHECKING([for host C++ compiler]) + if test -z "$NATIVE_CXX"; then + if test "$cross_compiling" = "yes"; then + AC_MSG_ERROR([Cross-compilation requires NATIVE_CXX to be set to host C++ compiler, cannot continue!]) + fi + NATIVE_CXX="$CXX" + fi + AC_MSG_RESULT([$NATIVE_CXX]) + + if test "$rust_uses_depends" = "yes"; then + RUSTUP_TOOLCHAIN="" + fi +fi +AC_SUBST(RUSTUP_TOOLCHAIN) + dnl check if libgmp is present TEMP_CPPFLAGS="$CPPFLAGS" TEMP_LDFLAGS="$LDFLAGS" @@ -1936,6 +2191,57 @@ if test "$build_bitcoin_wallet$build_bitcoin_cli$build_bitcoin_tx$build_bitcoin_ AC_MSG_ERROR([No targets! Please specify at least one of: --with-utils --with-libs --with-daemon --with-gui --enable-fuzz(-binary) --enable-bench or --enable-tests]) fi +dnl Dash Platform GUI support needs the GUI and the wallet, and mbedtls for the +dnl DAPI TLS client. The platform client library and the Platform-owned CXX +dnl archive are linked into dash-qt (and test binaries) only. +if test "$enable_platform_gui" = "yes"; then + if test "$bitcoin_enable_qt" != "yes"; then + AC_MSG_ERROR([--enable-platform-gui requires the GUI (--with-gui)]) + fi + if test "$enable_wallet" != "yes"; then + AC_MSG_ERROR([--enable-platform-gui requires wallet support (--enable-wallet)]) + fi + if test "$MBEDTLS_CFLAGS$MBEDTLS_LIBS" = ""; then + AC_CHECK_HEADER([mbedtls/ssl.h], [], [AC_MSG_ERROR([mbedtls headers not found (required by --enable-platform-gui)])]) + AC_CHECK_LIB([mbedcrypto], [main], [MBEDTLS_LIBS="-lmbedcrypto"], [AC_MSG_ERROR([libmbedcrypto not found (required by --enable-platform-gui)])]) + AC_CHECK_LIB([mbedx509], [main], [MBEDTLS_LIBS="-lmbedx509 $MBEDTLS_LIBS"], [AC_MSG_ERROR([libmbedx509 not found (required by --enable-platform-gui)])], [$MBEDTLS_LIBS]) + AC_CHECK_LIB([mbedtls], [mbedtls_ssl_init], [MBEDTLS_LIBS="-lmbedtls $MBEDTLS_LIBS"], [AC_MSG_ERROR([libmbedtls not found (required by --enable-platform-gui)])], [$MBEDTLS_LIBS]) + fi + if test -z "$PLATFORM_CXX_LIBS"; then + PLATFORM_CXX_LIBS="-ldash_platform_cxx" + fi + PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -lpthread -lm" + case $host in + *darwin*) PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -framework CoreFoundation" ;; + *linux*) PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -ldl" ;; + *mingw*) PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -luserenv -lntdll" ;; + esac + TEMP_CPPFLAGS="$CPPFLAGS" + TEMP_LIBS="$LIBS" + CPPFLAGS="$CPPFLAGS $PLATFORM_CXX_CFLAGS" + LIBS="$PLATFORM_CXX_LIBS $LIBS" + AC_LANG_PUSH([C++]) + AC_MSG_CHECKING([for Dash Platform CXX bindings]) + AC_LINK_IFELSE([AC_LANG_PROGRAM([[ + #include + #include + ]], [[ + platform_ffi::set_context("test", std::uint32_t{0}); + ]])], [AC_MSG_RESULT([yes])], [ + AC_MSG_RESULT([no]) + AC_MSG_ERROR([Dash Platform CXX bindings not found (required by --enable-platform-gui)]) + ]) + AC_LANG_POP + CPPFLAGS="$TEMP_CPPFLAGS" + LIBS="$TEMP_LIBS" + AC_DEFINE([ENABLE_PLATFORM_GUI], [1], [Define this symbol to enable Dash Platform support in the GUI]) +fi +AM_CONDITIONAL([ENABLE_PLATFORM_GUI], [test "$enable_platform_gui" = "yes"]) +AC_SUBST(MBEDTLS_CFLAGS) +AC_SUBST(MBEDTLS_LIBS) +AC_SUBST(PLATFORM_CXX_CFLAGS) +AC_SUBST(PLATFORM_CXX_LIBS) + AM_CONDITIONAL([TARGET_DARWIN], [test "$TARGET_OS" = "darwin"]) AM_CONDITIONAL([BUILD_DARWIN], [test "$BUILD_OS" = "darwin"]) AM_CONDITIONAL([TARGET_LINUX], [test "$TARGET_OS" = "linux"]) @@ -1953,6 +2259,7 @@ AM_CONDITIONAL([USE_QRCODE], [test "$use_qr" = "yes"]) AM_CONDITIONAL([USE_LCOV], [test "$use_lcov" = "yes"]) AM_CONDITIONAL([USE_LIBEVENT], [test "$use_libevent" = "yes"]) AM_CONDITIONAL([HARDEN], [test "$use_hardening" = "yes"]) +AM_CONDITIONAL([ENABLE_RUST], [test "$enable_rust" = "yes"]) AM_CONDITIONAL([ENABLE_SSSE3], [test "$enable_ssse3" = "yes"]) AM_CONDITIONAL([ENABLE_SSE42], [test "$enable_sse42" = "yes"]) AM_CONDITIONAL([ENABLE_SSE41], [test "$enable_sse41" = "yes"]) @@ -1998,6 +2305,17 @@ AC_SUBST(BITCOIN_WALLET_TOOL_NAME) AC_SUBST(BITCOIN_MP_NODE_NAME) AC_SUBST(BITCOIN_MP_GUI_NAME) +AC_SUBST(NATIVE_AR) +AC_SUBST(NATIVE_CC) +AC_SUBST(NATIVE_CXX) +AC_SUBST(RUST_LIBS) +AC_SUBST(RUST_MACOS_DEPLOYMENT_TARGET) +AC_SUBST(RUST_NATIVE) +AC_SUBST(RUST_OSX_SDK) +AC_SUBST(RUST_TARGET) +AC_SUBST(RUST_VENDORED_SOURCES) +AC_SUBST(RUSTUP_TOOLCHAIN) + AC_SUBST(RELDFLAGS) AC_SUBST(CORE_LDFLAGS) AC_SUBST(CORE_CPPFLAGS) @@ -2051,7 +2369,7 @@ AC_SUBST(HAVE_MM_PREFETCH) AC_SUBST(HAVE_STRONG_GETAUXVAL) AC_SUBST(ANDROID_ARCH) AC_SUBST(HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR) -AC_CONFIG_FILES([Makefile src/Makefile doc/man/Makefile share/setup.nsi share/qt/Info.plist test/config.ini]) +AC_CONFIG_FILES([Makefile rust/Makefile src/Makefile doc/man/Makefile share/setup.nsi share/qt/Info.plist test/config.ini]) AC_CONFIG_FILES([contrib/devtools/split-debug.sh],[chmod +x contrib/devtools/split-debug.sh]) AM_COND_IF([HAVE_DOXYGEN], [AC_CONFIG_FILES([doc/Doxyfile])]) AC_CONFIG_LINKS([contrib/filter-lcov.py:contrib/filter-lcov.py]) @@ -2077,7 +2395,9 @@ CPPFLAGS="$CPPFLAGS_TEMP" if test -n "$use_sanitizers"; then export SECP_CFLAGS="$SECP_CFLAGS $SANITIZER_CFLAGS" fi -ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --enable-module-recovery --disable-module-ecdh --disable-openssl-tests" +dnl The ECDH module is required by the wallet's Platform key provider +dnl (DashPay contact request encryption, wallet/platformkeys.cpp). +ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --enable-module-recovery --enable-module-ecdh --disable-openssl-tests" AC_CONFIG_SUBDIRS([src/dashbls src/secp256k1]) AC_OUTPUT @@ -2131,6 +2451,7 @@ echo " debug enabled = $enable_debug" echo " stacktraces = $enable_stacktraces" echo " crash hooks = $enable_crashhooks" echo " miner enabled = $enable_miner" +echo " platform gui = $enable_platform_gui" echo " werror = $enable_werror" echo echo " target os = $host_os" @@ -2145,3 +2466,19 @@ echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $CORE_LDFLAGS $BACKTRA echo " AR = $AR" echo " ARFLAGS = $ARFLAGS" echo +echo " CARGO = $CARGO" +echo " CARGO_INCREMENTAL = $CARGO_INCREMENTAL" +echo " CXXBRIDGE = $CXXBRIDGE" +echo " NATIVE_AR = $NATIVE_AR" +echo " NATIVE_CC = $NATIVE_CC" +echo " NATIVE_CXX = $NATIVE_CXX" +echo " PLATFORM_CXX_CFLAGS = $PLATFORM_CXX_CFLAGS" +echo " PLATFORM_CXX_LIBS = $PLATFORM_CXX_LIBS" +echo " RUSTC = $RUSTC" +echo " RUSTFLAGS = $RUSTFLAGS" +echo " RUST_MACOS_DEPLOYMENT_TARGET = $RUST_MACOS_DEPLOYMENT_TARGET" +echo " RUST_NATIVE = $RUST_NATIVE" +echo " RUST_OSX_SDK = $RUST_OSX_SDK" +echo " RUST_TARGET = $RUST_TARGET" +echo " RUST_VENDORED_SOURCES = $RUST_VENDORED_SOURCES" +echo diff --git a/contrib/containers/guix/scripts/guix-start b/contrib/containers/guix/scripts/guix-start index 42300bc453b6..53a3a96080bd 100755 --- a/contrib/containers/guix/scripts/guix-start +++ b/contrib/containers/guix/scripts/guix-start @@ -26,6 +26,8 @@ fi cd "${WORKSPACE_PATH}" git status >> /dev/null +export PLATFORM_GUI="${PLATFORM_GUI:-}" + export HOSTS="${HOSTS:-x86_64-linux-gnu aarch64-linux-gnu riscv64-linux-gnu x86_64-w64-mingw32 x86_64-apple-darwin arm64-apple-darwin}" diff --git a/contrib/devtools/cargo-vendor-git-sources.sh b/contrib/devtools/cargo-vendor-git-sources.sh new file mode 100755 index 000000000000..7028a402bca4 --- /dev/null +++ b/contrib/devtools/cargo-vendor-git-sources.sh @@ -0,0 +1,39 @@ +#!/bin/sh +export LC_ALL=C + +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# +# Emit cargo source-replacement stanzas for every git source in a Cargo.lock, +# for appending to an offline .cargo/config.toml whose vendored-sources +# directory holds the output of `cargo vendor`. `cargo vendor` prints these +# stanzas itself, but only at vendor time; deriving them from the lockfile +# lets the build system reconstruct the config without re-running vendor. +# +# usage: cargo-vendor-git-sources.sh path/to/Cargo.lock + +set -eu + +if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then + echo "usage: $0 path/to/Cargo.lock" >&2 + exit 1 +fi + +# Lockfile entries look like: +# source = "git+https://github.com/dashpay/platform?tag=v4.1.0#bfc80249..." +# The stanza key is the source without the fragment; the query parameter +# (tag=/branch=/rev=), when present, becomes a key of the same name. +grep '^source = "git+' "$1" | sed 's/^source = "//; s/"$//; s/#.*//' | sort -u | \ +while IFS= read -r src; do + url_query="${src#git+}" + url="${url_query%%\?*}" + printf '\n[source."%s"]\ngit = "%s"\n' "$src" "$url" + case "$url_query" in + *\?*) + query="${url_query#*\?}" + printf '%s = "%s"\n' "${query%%=*}" "${query#*=}" + ;; + esac + printf 'replace-with = "vendored-sources"\n' +done diff --git a/contrib/devtools/circular-dependencies.py b/contrib/devtools/circular-dependencies.py index a6cdc343d5cb..6cbbdb189eea 100755 --- a/contrib/devtools/circular-dependencies.py +++ b/contrib/devtools/circular-dependencies.py @@ -11,6 +11,10 @@ MAPPING = { 'core_read.cpp': 'core_io.cpp', 'core_write.cpp': 'core_io.cpp', + 'evo/core_write.cpp': 'core_io.cpp', + 'evo/providertx_util.cpp': 'evo/providertx.cpp', + 'llmq/core_write.cpp': 'core_io.cpp', + 'qt/guiutil_font.cpp': 'qt/guiutil.cpp', } # Directories with header-based modules, where the assumption that .cpp files diff --git a/contrib/devtools/update-native-cxxbridge.py b/contrib/devtools/update-native-cxxbridge.py new file mode 100755 index 000000000000..319fe9ada6b3 --- /dev/null +++ b/contrib/devtools/update-native-cxxbridge.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import hashlib +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +import urllib.request +from pathlib import Path + + +def get_cxx_version(makefile_path: Path) -> str: + content = makefile_path.read_text() + match = re.search(r"\$\(package\)_version:=(.+)", content) + if not match: + raise RuntimeError("Could not find cxx version in makefile") + return match.group(1).strip() + + +def download_and_hash(url: str, dest: Path) -> str: + hasher = hashlib.sha256() + dest.parent.mkdir(parents=True, exist_ok=True) + temporary_path = None + try: + with tempfile.NamedTemporaryFile(dir=dest.parent, delete=False) as output: + temporary_path = Path(output.name) + with urllib.request.urlopen(url) as response: + while chunk := response.read(8192): + hasher.update(chunk) + output.write(chunk) + temporary_path.replace(dest) + except BaseException: + if temporary_path is not None: + temporary_path.unlink(missing_ok=True) + raise + return hasher.hexdigest() + + +def write_stamp(stamps_dir: Path, version: str, sha256: str, file_name: str) -> None: + stamps_dir.mkdir(parents=True, exist_ok=True) + stamp_path = stamps_dir / f".stamp_fetched-native_cxxbridge-{version}-{sha256}.hash" + stamp_path.write_text(f"{sha256} {file_name}\n") + + +def update_value_in_file(path: Path, pattern: str, value: str) -> None: + content = path.read_text() + regex = re.compile(pattern, re.MULTILINE) + new_content, replacements = regex.subn( + lambda match: f"{match.group(1)}{value}{match.group(2) if match.lastindex == 2 else ''}", content + ) + if replacements != 1: + raise RuntimeError(f"Expected one matching value in {path}, found {replacements}") + path.write_text(new_content) + + +def main() -> int: + script_dir = Path(__file__).resolve().parent + repo_root = script_dir / "../.." + repo_root = repo_root.resolve() + + makefile_path = repo_root / "depends/packages/native_cxxbridge.mk" + if not makefile_path.exists(): + print(f"Error: {makefile_path} not found", file=sys.stderr) + return 1 + + version = get_cxx_version(makefile_path) + print(f"cxx version: {version}") + + sources_dir = repo_root / "depends/sources" + file_name = f"native_cxxbridge-{version}.tar.gz" + tarball_path = sources_dir / file_name + url = f"https://github.com/dtolnay/cxx/archive/refs/tags/{version}.tar.gz" + print(f"Downloading {url}") + hash_value = download_and_hash(url, tarball_path) + print(f"sha256: {hash_value}") + + toolchain_path = repo_root / "rust-toolchain.toml" + if not toolchain_path.exists(): + print(f"Error: {toolchain_path} not found", file=sys.stderr) + return 1 + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) + print(f"Working in {tmp_path}") + + # Copy rust-toolchain.toml + shutil.copy(toolchain_path, tmp_path / "rust-toolchain.toml") + + # Extract tarball + print(f"Extracting {tarball_path}") + with tarfile.open(tarball_path, "r:gz") as tar: + # getattr keeps this compatible with the older tarfile type stubs + # used by the Python 3.10 lint environment. Supported Python 3.10 + # releases include the security filter at runtime. + getattr(tar, "extractall")(tmp_path, filter="data") + + cxx_dir = tmp_path / f"cxx-{version}" + if not cxx_dir.exists(): + print(f"Error: Expected directory {cxx_dir} not found after extraction", file=sys.stderr) + return 1 + + # Copy rust-toolchain.toml into cxx directory + shutil.copy(toolchain_path, cxx_dir / "rust-toolchain.toml") + + # Run cargo check + print("Running cargo check --release --package=cxxbridge-cmd --bin=cxxbridge") + result = subprocess.run( + ["cargo", "check", "--release", "--package=cxxbridge-cmd", "--bin=cxxbridge"], + cwd=cxx_dir, + ) + if result.returncode != 0: + print("Error: cargo check failed", file=sys.stderr) + return 1 + + # Copy Cargo.lock to patches directory + cargo_lock_src = cxx_dir / "Cargo.lock" + cargo_lock_dst = repo_root / "depends/patches/native_cxxbridge/Cargo.lock" + if not cargo_lock_src.exists(): + print(f"Error: {cargo_lock_src} not found after cargo check", file=sys.stderr) + return 1 + + print("Updating the workspace cxx crates") + result = subprocess.run(["cargo", "update", "-p", "cxx", "--precise", version], cwd=repo_root) + if result.returncode != 0: + print("Error: workspace cargo update failed", file=sys.stderr) + return 1 + + cargo_lock_dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(cargo_lock_src, cargo_lock_dst) + print(f"Copied Cargo.lock to {cargo_lock_dst}") + + update_value_in_file(makefile_path, r"^(\$\(package\)_sha256_hash:=).*$", hash_value) + configure_path = repo_root / "configure.ac" + update_value_in_file(configure_path, r'^(CXXBRIDGE_REQUIRED_VERSION=")[^"]*(")$', version) + write_stamp(sources_dir / "download-stamps", version, hash_value, file_name) + + print("\nDone!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contrib/devtools/update-rust-hashes.py b/contrib/devtools/update-rust-hashes.py new file mode 100755 index 000000000000..a2cee608f33e --- /dev/null +++ b/contrib/devtools/update-rust-hashes.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021-2022 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import hashlib +import re +import sys +import urllib.request +from pathlib import Path + +# Rust standard libraries provisioned in rust_stdlib.mk. Confined to the +# hosts we validate (the Guix release set); see rust_stdlib.mk. +CROSS_TARGETS = [ + # Linux + "aarch64-unknown-linux-musl", + "riscv64gc-unknown-linux-musl", + "x86_64-unknown-linux-musl", + # Windows + "x86_64-pc-windows-gnu", + # macOS + "aarch64-apple-darwin", + "x86_64-apple-darwin", +] + +# Native compilers provisioned in native_rust.mk (build hosts for depends) +NATIVE_TARGETS = [ + # Linux + ("aarch64-unknown-linux-gnu", "aarch64_linux"), + ("x86_64-unknown-linux-gnu", "x86_64_linux"), + # macOS + ("aarch64-apple-darwin", "aarch64_darwin"), + ("x86_64-apple-darwin", "x86_64_darwin"), +] + + +def get_rust_version(makefile_path: Path) -> str: + content = makefile_path.read_text() + match = re.search(r"\$\(package\)_version:=(.+)", content) + if not match: + raise RuntimeError("Could not find Rust version in makefile") + return match.group(1).strip() + + +def compute_sha256(url: str) -> str: + hasher = hashlib.sha256() + with urllib.request.urlopen(url) as response: + while chunk := response.read(8192): + hasher.update(chunk) + return hasher.hexdigest() + + +def update_hash_in_file(makefile_path: Path, pattern: str, new_hash: str) -> None: + content = makefile_path.read_text() + regex = re.compile(rf"^(\$\(package\)_{pattern}:=).*$", re.MULTILINE) + if not regex.search(content): + raise RuntimeError(f"Could not find pattern {pattern} in makefile") + new_content = regex.sub(rf"\g<1>{new_hash}", content) + makefile_path.write_text(new_content) + + +def update_version_in_file(path: Path, pattern: str, version: str) -> None: + content = path.read_text() + regex = re.compile(pattern, re.MULTILINE) + new_content, replacements = regex.subn( + lambda match: f"{match.group(1)}{version}{match.group(2) if match.lastindex == 2 else ''}", content + ) + if replacements != 1: + raise RuntimeError(f"Expected one version pin in {path}, found {replacements}") + path.write_text(new_content) + + +def compute_rust_hash(rust_version: str, rust_target: str) -> str: + url = f"https://static.rust-lang.org/dist/rust-{rust_version}-{rust_target}.tar.gz" + return compute_sha256(url) + + +def compute_stdlib_hash(rust_version: str, rust_target: str) -> str: + url = f"https://static.rust-lang.org/dist/rust-std-{rust_version}-{rust_target}.tar.gz" + return compute_sha256(url) + + +def main() -> int: + script_dir = Path(__file__).resolve().parent + native_rust_path = script_dir / "../../depends/packages/native_rust.mk" + native_rust_path = native_rust_path.resolve() + rust_stdlib_path = script_dir / "../../depends/packages/rust_stdlib.mk" + rust_stdlib_path = rust_stdlib_path.resolve() + toolchain_path = (script_dir / "../../rust-toolchain.toml").resolve() + configure_path = (script_dir / "../../configure.ac").resolve() + + for path in (native_rust_path, rust_stdlib_path, toolchain_path, configure_path): + if not path.exists(): + print(f"Error: {path} not found", file=sys.stderr) + return 1 + + rust_version = get_rust_version(native_rust_path) + + print(f"Rust version: {rust_version}\n") + print("Downloading native compiler hashes:") + + native_hashes = {} + for rust_target, makefile_id in NATIVE_TARGETS: + native_hashes[makefile_id] = compute_rust_hash(rust_version, rust_target) + print(f" Downloaded sha256_hash_{makefile_id}") + + print("\nDownloading stdlib hashes:") + stdlib_hashes = {} + for rust_target in CROSS_TARGETS: + stdlib_hashes[rust_target] = compute_stdlib_hash(rust_version, rust_target) + print(f" Downloaded sha256_hash_{rust_target}") + + for makefile_id, hash_value in native_hashes.items(): + update_hash_in_file(native_rust_path, f"sha256_hash_{makefile_id}", hash_value) + for rust_target, hash_value in stdlib_hashes.items(): + update_hash_in_file(rust_stdlib_path, f"sha256_hash_{rust_target}", hash_value) + + update_version_in_file(rust_stdlib_path, r"^(\$\(package\)_version:=).*$", rust_version) + update_version_in_file(toolchain_path, r'^(channel = ")[^"]*(")$', rust_version) + update_version_in_file(configure_path, r'^(RUSTC_REQUIRED_VERSION=")[^"]*(")$', rust_version) + print("\nSynchronized rust_stdlib.mk, rust-toolchain.toml, and configure.ac") + + print("\nDone!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contrib/guix/guix-build b/contrib/guix/guix-build index 80b719eb29d8..53b4b118428c 100755 --- a/contrib/guix/guix-build +++ b/contrib/guix/guix-build @@ -302,9 +302,16 @@ mkdir -p "$OUTDIR_BASE" # Download the depends sources now as we won't have internet access in the build # container for host in $HOSTS; do - make -C "${PWD}/depends" -j"$JOBS" download-"$(host_to_commonname "$host")" ${V:+V=1} ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} + make -C "${PWD}/depends" -j"$JOBS" download-"$(host_to_commonname "$host")" ${V:+V=1} ${PLATFORM_GUI:+PLATFORM_GUI=1} ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} done +# Rust standard libraries and vendored crates are host-independent and need +# network (and git) access, so they must be fetched once out here as well — +# the build container is offline. +if [ -n "${PLATFORM_GUI}" ]; then + make -C "${PWD}/depends" -j"$JOBS" PLATFORM_GUI=1 download-rust-std vendor-crates ${V:+V=1} ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} +fi + # Usage: outdir_for_host HOST SUFFIX # # HOST: The current platform triple we're building for @@ -466,6 +473,7 @@ EOF ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} \ ${BASE_CACHE:+BASE_CACHE="$BASE_CACHE"} \ ${SDK_PATH:+SDK_PATH="$SDK_PATH"} \ + ${PLATFORM_GUI:+PLATFORM_GUI=1} \ DISTSRC="$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")" \ OUTDIR="$(OUTDIR_BASE=/outdir-base && outdir_for_host "$HOST")" \ DIST_ARCHIVE_BASE=/outdir-base/dist-archive \ diff --git a/contrib/guix/libexec/build.sh b/contrib/guix/libexec/build.sh index 0c9fda7ad11d..92b6109a6000 100755 --- a/contrib/guix/libexec/build.sh +++ b/contrib/guix/libexec/build.sh @@ -173,6 +173,7 @@ export TZ="UTC" # Build the depends tree, overriding variables that assume multilib gcc make -C depends --jobs="$JOBS" HOST="$HOST" \ ${V:+V=1} \ + ${PLATFORM_GUI:+PLATFORM_GUI=1} \ ${SOURCES_PATH+SOURCES_PATH="$SOURCES_PATH"} \ ${BASE_CACHE+BASE_CACHE="$BASE_CACHE"} \ ${SDK_PATH+SDK_PATH="$SDK_PATH"} \ diff --git a/contrib/guix/manifest.scm b/contrib/guix/manifest.scm index 2f21f5b58801..9d2b70ccaac8 100644 --- a/contrib/guix/manifest.scm +++ b/contrib/guix/manifest.scm @@ -5,6 +5,7 @@ ((gnu packages certs) #:select (nss-certs)) ((gnu packages check) #:select (libfaketime)) ((gnu packages cmake) #:select (cmake-minimal)) + ((gnu packages elf) #:select (patchelf)) (gnu packages commencement) (gnu packages compression) (gnu packages cross-base) @@ -525,6 +526,7 @@ inspecting signatures in Mach-O binaries.") ;; Compression and archiving tar gzip + lbzip2 xz ;; Build tools gcc-toolchain-13 @@ -534,6 +536,8 @@ inspecting signatures in Mach-O binaries.") autoconf-2.71 automake pkg-config + patchelf + zlib ;; Scripting python-minimal ;; (3.10) ;; Git diff --git a/depends/Makefile b/depends/Makefile index c8510f4cc010..718d64e26835 100644 --- a/depends/Makefile +++ b/depends/Makefile @@ -44,6 +44,8 @@ NO_UPNP ?= NO_USDT ?= NO_NATPMP ?= MULTIPROCESS ?= +RUST ?= +PLATFORM_GUI ?= LTO ?= NO_HARDEN ?= FALLBACK_DOWNLOAD_PATH ?= http://dash-depends-sources.s3-website-us-west-2.amazonaws.com @@ -60,6 +62,12 @@ DOWNLOAD_RETRIES:=3 HOST_ID_SALT ?= salt BUILD_ID_SALT ?= salt +CRATE_REGISTRY := vendored-sources +CRATE_LOCK = $(BASEDIR)/../Cargo.lock +CRATE_LOCK_HASH = $(shell $(build_SHA256SUM) $(CRATE_LOCK) | cut -c-$(HASH_LENGTH)) +CRATE_ARCHIVE = $(SOURCES_PATH)/vendored-crates-$(CRATE_LOCK_HASH).tar.gz +CRATE_REGISTRY_STAMP = $(host_prefix)/.$(CRATE_REGISTRY).stamp + ifneq ($(DEBUG),) release_type=debug else @@ -175,10 +183,18 @@ natpmp_packages_$(NO_NATPMP) = $(natpmp_packages) zmq_packages_$(NO_ZMQ) = $(zmq_packages) multiprocess_packages_$(MULTIPROCESS) = $(multiprocess_packages) +platform_packages_$(PLATFORM_GUI) = $(platform_packages) usdt_packages_$(NO_USDT) = $(usdt_$(host_os)_packages) packages += $($(host_arch)_$(host_os)_packages) $($(host_os)_packages) $(boost_packages_) $(libevent_packages_) $(qt_packages_) $(wallet_packages_) $(upnp_packages_) $(natpmp_packages_) $(usdt_packages_) native_packages += $($(host_arch)_$(host_os)_native_packages) $($(host_os)_native_packages) +ifeq ($(RUST),1) +packages += $(rust_packages) +native_packages += $(rust_native_packages) +rust_download_targets = download-rust-std vendor-crates +endif + +cargo_packages = $(filter native_cxxbridge,$(native_packages)) $(filter platform_cxx,$(packages)) ifneq ($(zmq_packages_),) packages += $(zmq_packages) @@ -189,6 +205,11 @@ packages += $(multiprocess_packages) native_packages += $(multiprocess_native_packages) endif +ifeq ($(platform_packages_),) +packages += $(platform_packages) +native_packages += $(platform_native_packages) +endif + all_packages = $(packages) $(native_packages) meta_depends = Makefile config.guess config.sub funcs.mk builders/default.mk hosts/default.mk hosts/$(host_os).mk builders/$(build_os).mk @@ -202,9 +223,39 @@ $(host_prefix)/.stamp_$(final_build_id): $(native_packages) $(packages) mkdir -p $(@D) echo copying packages: $^ echo to: $(@D) - cd $(@D); $(foreach package,$^, $(build_TAR) xf $($(package)_cached); ) + cd $(@D); $(foreach package,$^, $(build_TAR) --no-same-owner -xf $($(package)_cached); ) touch $@ +# Vendors Rust crate dependencies for offline builds +# Uses pre-built archive if available, otherwise runs 'cargo vendor' +$(CRATE_REGISTRY_STAMP): $(host_prefix)/.stamp_$(final_build_id) $(CRATE_LOCK) + @rm -rf $(host_prefix)/$(CRATE_REGISTRY) + @if test -f $(CRATE_ARCHIVE); \ + then echo Extracting pre-vendored crates from $(CRATE_ARCHIVE)...; \ + $(build_TAR) --no-same-owner -xf $(CRATE_ARCHIVE) -C $(host_prefix); \ + else echo Vendoring crates...; \ + $(host_prefix)/native/bin/cargo vendor --locked --manifest-path $(BASEDIR)/../Cargo.toml $(host_prefix)/$(CRATE_REGISTRY); \ + fi + @test -d $(host_prefix)/$(CRATE_REGISTRY) + @touch $@ + +# Vendors crates for cargo packages +vendor-dep-crates: $(foreach package,$(cargo_packages),vendor-$(package)-crates) + +# Vendors project-wide cargo packages +vendor-all-crates: $(native_rust_cached) $(CRATE_LOCK) + @rm -rf $(WORK_PATH)/vendor-main + @mkdir -p $(WORK_PATH)/vendor-main + @$(build_TAR) --no-same-owner -xf $(native_rust_cached) -C $(WORK_PATH)/vendor-main + @echo "Vendoring main project crates..." + @$(WORK_PATH)/vendor-main/native/bin/cargo vendor --locked --manifest-path $(BASEDIR)/../Cargo.toml $(WORK_PATH)/vendor-main/$(CRATE_REGISTRY) + @cd $(WORK_PATH)/vendor-main; find $(CRATE_REGISTRY) | sort | $(build_TAR) --no-recursion -czf $(CRATE_ARCHIVE) -T - + @rm -rf $(WORK_PATH)/vendor-main + @echo "Created $(CRATE_ARCHIVE)" + +# Vendors everything +vendor-crates: vendor-dep-crates vendor-all-crates + # $PATH is not preserved between ./configure and make by convention. Its # modification and overriding at ./configure time is (as I understand it) # supposed to be captured by the AC_{PROG_{,OBJ}CXX,PATH_{PROG,TOOL}} macros, @@ -232,13 +283,18 @@ $(host_prefix)/share/config.site : config.site.in $(host_prefix)/.stamp_$(final_ @mkdir -p $(@D) sed -e 's|@HOST@|$(host)|' \ -e 's|@CC@|$(host_CC)|' \ + -e 's|@NATIVE_CC@|$(build_CC)|' \ -e 's|@CXX@|$(host_CXX)|' \ + -e 's|@NATIVE_CXX@|$(build_CXX)|' \ -e 's|@AR@|$(host_AR)|' \ + -e 's|@NATIVE_AR@|$(build_AR)|' \ -e 's|@RANLIB@|$(host_RANLIB)|' \ -e 's|@NM@|$(host_NM)|' \ -e 's|@STRIP@|$(host_STRIP)|' \ -e 's|@OBJDUMP@|$(host_OBJDUMP)|' \ -e 's|@DSYMUTIL@|$(host_DSYMUTIL)|' \ + -e 's|@OSX_MIN_VERSION@|$(OSX_MIN_VERSION)|' \ + -e 's|@OSX_SDK@|$(OSX_SDK)|' \ -e 's|@WINDRES@|$(host_WINDRES)|' \ -e 's|@build_os@|$(build_os)|' \ -e 's|@host_os@|$(host_os)|' \ @@ -257,6 +313,8 @@ $(host_prefix)/share/config.site : config.site.in $(host_prefix)/.stamp_$(final_ -e 's|@no_usdt@|$(NO_USDT)|' \ -e 's|@no_natpmp@|$(NO_NATPMP)|' \ -e 's|@multiprocess@|$(MULTIPROCESS)|' \ + -e 's|@rust@|$(RUST)|' \ + -e 's|@platform_gui@|$(PLATFORM_GUI)|' \ -e 's|@lto@|$(LTO)|' \ -e 's|@no_harden@|$(NO_HARDEN)|' \ -e 's|@debug@|$(DEBUG)|' \ @@ -293,21 +351,29 @@ clean-all: clean clean: @rm -rf $(WORK_PATH) $(BASE_CACHE) $(BUILD) *.log +ifeq ($(RUST),1) +install: check-packages $(host_prefix)/share/config.site $(CRATE_REGISTRY_STAMP) +else install: check-packages $(host_prefix)/share/config.site +endif download-one: check-sources $(all_sources) download-osx: - @$(MAKE) -s HOST=x86_64-apple-darwin download-one + @$(MAKE) -s RUST=$(RUST) HOST=x86_64-apple-darwin download-one download-linux: - @$(MAKE) -s HOST=x86_64-unknown-linux-gnu download-one + @$(MAKE) -s RUST=$(RUST) HOST=x86_64-unknown-linux-gnu download-one download-win: - @$(MAKE) -s HOST=x86_64-w64-mingw32 download-one -download: download-osx download-linux download-win + @$(MAKE) -s RUST=$(RUST) HOST=x86_64-w64-mingw32 download-one +download-rust-std: + @mkdir -p $(SOURCES_PATH) + @mkdir -p $(SOURCES_PATH)/download-stamps + @$(foreach target,$(rust_stdlib_targets),$(call download_rust_std_target,$(target)) && ) true +download: download-osx download-linux download-win $(rust_download_targets) $(foreach package,$(all_packages),$(eval $(call ext_add_stages,$(package)))) -.PHONY: install cached clean clean-all download-one download-osx download-linux download-win download check-packages check-sources +.PHONY: install cached clean clean-all download-one download-osx download-linux download-win download download-rust-std check-packages check-sources vendor-crates vendor-dep-crates vendor-all-crates .PHONY: FORCE $(V).SILENT: diff --git a/depends/README.md b/depends/README.md index f504627729f0..dbb285c22023 100644 --- a/depends/README.md +++ b/depends/README.md @@ -92,6 +92,8 @@ The following can be set when running make: `make FOO=bar` build script logic) are searched for among the host system packages using `pkg-config`. It allows building with packages of other (newer) versions - `MULTIPROCESS`: build libmultiprocess (experimental, requires cmake) +- `RUST`: Download/build/cache the Rust toolchain, target standard library, `cxxbridge` + and vendored crate sources needed for `--enable-rust` (see [doc/rust.md](../doc/rust.md)) - `DEBUG`: Disable some optimizations and enable more runtime checking - `HOST_ID_SALT`: Optional salt to use when generating host package ids - `BUILD_ID_SALT`: Optional salt to use when generating build package ids diff --git a/depends/config.site.in b/depends/config.site.in index 398a09b74c63..28c1defd8161 100644 --- a/depends/config.site.in +++ b/depends/config.site.in @@ -50,6 +50,21 @@ if test -z "$enable_multiprocess" && test -n "@multiprocess@"; then enable_multiprocess=yes fi +if test -z "$enable_rust" && test -n "@rust@"; then + enable_rust=yes +fi + +if test -z "$enable_platform_gui" && test -n "@platform_gui@"; then + enable_platform_gui=yes +fi + +if test -z "$PLATFORM_CXX_CFLAGS" && test -f "${depends_prefix}/include/dash/platform/ffi.h"; then + PLATFORM_CXX_CFLAGS="-I${depends_prefix}/include" +fi +if test -z "$PLATFORM_CXX_LIBS" && test -f "${depends_prefix}/lib/libdash_platform_cxx.a"; then + PLATFORM_CXX_LIBS="${depends_prefix}/lib/libdash_platform_cxx.a" +fi + if test -z "$with_miniupnpc" && test -n "@no_upnp@"; then with_miniupnpc=no fi @@ -95,14 +110,25 @@ LDFLAGS="-L${depends_prefix}/lib ${LDFLAGS}" if test -n "@CC@" -a -z "${CC}"; then CC="@CC@" fi +if test -n "@NATIVE_CC@" -a -z "${NATIVE_CC}"; then + NATIVE_CC="@NATIVE_CC@" +fi + if test -n "@CXX@" -a -z "${CXX}"; then CXX="@CXX@" fi +if test -n "@NATIVE_CXX@" -a -z "${NATIVE_CXX}"; then + NATIVE_CXX="@NATIVE_CXX@" +fi if test -n "@AR@"; then AR="@AR@" ac_cv_path_AR="${AR}" fi +if test -n "@NATIVE_AR@"; then + NATIVE_AR="@NATIVE_AR@" + ac_cv_path_NATIVE_AR="${NATIVE_AR}" +fi if test -n "@RANLIB@"; then RANLIB="@RANLIB@" @@ -129,6 +155,14 @@ if test "@host_os@" = darwin; then DSYMUTIL="@DSYMUTIL@" ac_cv_path_DSYMUTIL="${DSYMUTIL}" fi + + if test -n "@OSX_MIN_VERSION@"; then + OSX_MIN_VERSION="@OSX_MIN_VERSION@" + fi + + if test -n "@OSX_SDK@"; then + OSX_SDK="@OSX_SDK@" + fi fi if test "@host_os@" = mingw32; then @@ -154,3 +188,16 @@ fi if test -n "@LDFLAGS@"; then LDFLAGS="@LDFLAGS@ ${LDFLAGS}" fi + +if test -x "${depends_prefix}/native/bin/cargo"; then + CARGO="${depends_prefix}/native/bin/cargo" +fi +if test -x "${depends_prefix}/native/bin/rustc"; then + RUSTC="${depends_prefix}/native/bin/rustc" +fi +if test -x "${depends_prefix}/native/bin/cxxbridge"; then + CXXBRIDGE="${depends_prefix}/native/bin/cxxbridge" +fi +if test -z "$RUST_VENDORED_SOURCES"; then + RUST_VENDORED_SOURCES="${depends_prefix}/vendored-sources" +fi diff --git a/depends/funcs.mk b/depends/funcs.mk index 566f83a9868e..0fc9aee733df 100644 --- a/depends/funcs.mk +++ b/depends/funcs.mk @@ -197,6 +197,22 @@ $(1)_cmake += -DCMAKE_C_COMPILER_TARGET=$(host) $(1)_cmake += -DCMAKE_CXX_COMPILER_TARGET=$(host) endif endif + +$(1)_cargo=env CC="$$($(1)_cc)" \ + CXX="$$($(1)_cxx)" \ + AR="$$($(1)_ar)" \ + CFLAGS="$$($(1)_cppflags) $$($(1)_cflags)" \ + CXXFLAGS="$$($(1)_cppflags) $$($(1)_cxxflags)" \ + LDFLAGS="$$($(1)_ldflags)" \ + RUSTFLAGS="-C linker=$$(firstword $($(1)_cc))" \ + LD_LIBRARY_PATH="$$($($(1)_type)_prefix)/lib" +ifeq ($(host_os),darwin) +$(1)_cargo += MACOSX_DEPLOYMENT_TARGET="$(OSX_MIN_VERSION)" +ifneq ($(host),$(build)) +$(1)_cargo += SDKROOT="$(OSX_SDK)" +endif +endif +$(1)_cargo += cargo endef define int_add_cmds @@ -269,6 +285,39 @@ $(foreach stage,$(stages), .PHONY: $(1)_$(stage)) endef +# Template for vendoring a package's Rust crate dependencies +# Packages opt-in by defining $(package)_vendored_file_name and $(package)_cargo_manifest +define int_vendor_crates +ifneq ($($(1)_vendored_file_name),) +$(1)_vendored_archive = $(SOURCES_PATH)/$($(1)_vendored_file_name) + +vendor-$(1)-crates: $(native_rust_cached) $($(1)_fetched) + @rm -rf $(WORK_PATH)/vendor-$(1) + @mkdir -p $(WORK_PATH)/vendor-$(1) + @$(build_TAR) --no-same-owner -xf $(native_rust_cached) -C $(WORK_PATH)/vendor-$(1) + @echo "Vendoring $(1) crates..." + @mkdir -p $(WORK_PATH)/vendor-$(1)/src + @cd $(WORK_PATH)/vendor-$(1)/src && $(build_TAR) --no-same-owner --strip-components=1 -xf $(SOURCES_PATH)/$($(1)_file_name) + @if test -f $(PATCHES_PATH)/$(1)/Cargo.lock; then \ + cp $(PATCHES_PATH)/$(1)/Cargo.lock $(WORK_PATH)/vendor-$(1)/src/$($(1)_cargo_lock_path); \ + fi + @$(WORK_PATH)/vendor-$(1)/native/bin/cargo vendor --locked --manifest-path $(WORK_PATH)/vendor-$(1)/src/$($(1)_cargo_manifest) $(WORK_PATH)/vendor-$(1)/src/vendored + @cd $(WORK_PATH)/vendor-$(1)/src; find vendored | sort | $(build_TAR) --no-recursion -czf $$($(1)_vendored_archive) -T - + @rm -rf $(WORK_PATH)/vendor-$(1) + @echo "Created $$($(1)_vendored_archive)" +.PHONY: vendor-$(1)-crates +endif +endef + +define download_rust_std_target +([ -f "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" ] && \ + echo "Already have rust-std-$(rust_stdlib_version)-$(1).tar.gz" || \ + (echo "Downloading rust-std-$(rust_stdlib_version)-$(1).tar.gz..." && \ + $(build_DOWNLOAD) "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" "$(rust_stdlib_download_path)/rust-std-$(rust_stdlib_version)-$(1).tar.gz")) && \ +echo "$(rust_stdlib_sha256_hash_$(1)) $(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" | $(build_SHA256SUM) -c - && \ +echo "$(rust_stdlib_sha256_hash_$(1)) rust-std-$(rust_stdlib_version)-$(1).tar.gz" > "$(SOURCES_PATH)/download-stamps/.stamp_fetched-rust_stdlib-$(rust_stdlib_version)-$(rust_stdlib_sha256_hash_$(1)).hash" +endef + # These functions create the build targets for each package. They must be # broken down into small steps so that each part is done for all packages # before moving on to the next step. Otherwise, a package's info @@ -286,6 +335,18 @@ $(foreach package,$(all_packages),$(eval $(call int_vars,$(package)))) $(foreach native_package,$(native_packages),$(eval include packages/$(native_package).mk)) $(foreach package,$(packages),$(eval include packages/$(package).mk)) +# Extend preprocess_cmds for cargo packages to extract vendored crates +define int_cargo_preprocess_ext +$(1)_preprocess_cmds += && \ + if test -f $(SOURCES_PATH)/$($(1)_vendored_file_name); then \ + echo "Extracting vendored crates for $(1)..." && \ + $(build_TAR) --no-same-owner -xf $(SOURCES_PATH)/$($(1)_vendored_file_name) && \ + mkdir -p .cargo && \ + cp $(PATCHES_PATH)/$(1)/cargo-config.toml .cargo/config.toml; \ + fi +endef +$(foreach cargo_package,$(cargo_packages),$(eval $(call int_cargo_preprocess_ext,$(cargo_package)))) + #compute a hash of all files that comprise this package's build recipe $(foreach package,$(all_packages),$(eval $(call int_get_build_recipe_hash,$(package)))) @@ -297,3 +358,6 @@ $(foreach package,$(all_packages),$(eval $(call int_config_attach_build_config,$ #create build targets $(foreach package,$(all_packages),$(eval $(call int_add_cmds,$(package)))) + +#create vendor targets for cargo packages +$(foreach cargo_package,$(cargo_packages),$(eval $(call int_vendor_crates,$(cargo_package)))) diff --git a/depends/packages/mbedtls.mk b/depends/packages/mbedtls.mk new file mode 100644 index 000000000000..cea4466c167d --- /dev/null +++ b/depends/packages/mbedtls.mk @@ -0,0 +1,27 @@ +package=mbedtls +$(package)_version=3.6.3.1 +$(package)_download_path=https://github.com/Mbed-TLS/mbedtls/releases/download/v$($(package)_version)/ +$(package)_file_name=$(package)-$($(package)_version).tar.bz2 +$(package)_sha256_hash=243ed496d5f88a5b3791021be2800aac821b9a4cc16e7134aa413c58b4c20e0c + +define $(package)_set_vars +$(package)_config_opts := -DENABLE_PROGRAMS=OFF -DENABLE_TESTING=OFF +$(package)_config_opts += -DUSE_SHARED_MBEDTLS_LIBRARY=OFF -DUSE_STATIC_MBEDTLS_LIBRARY=ON +$(package)_config_opts += -DMBEDTLS_FATAL_WARNINGS=OFF -DGEN_FILES=OFF +endef + +define $(package)_config_cmds + $($(package)_cmake) -S . -B . +endef + +define $(package)_build_cmds + $(MAKE) +endef + +define $(package)_stage_cmds + $(MAKE) DESTDIR=$($(package)_staging_dir) install +endef + +define $(package)_postprocess_cmds + rm -rf lib/cmake +endef diff --git a/depends/packages/native_cxxbridge.mk b/depends/packages/native_cxxbridge.mk new file mode 100644 index 000000000000..c379c428f0cb --- /dev/null +++ b/depends/packages/native_cxxbridge.mk @@ -0,0 +1,35 @@ +# Copyright (c) 2022-2025 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# To update the package, change the version below and then run the script +# ./contrib/devtools/update-native-cxxbridge.py + +package:=native_cxxbridge +$(package)_version:=1.0.198 +$(package)_download_path:=https://github.com/dtolnay/cxx/archive/refs/tags +$(package)_file_name:=native_cxxbridge-$($(package)_version).tar.gz +$(package)_download_file:=$($(package)_version).tar.gz +$(package)_sha256_hash:=e8c54f11e12b00f80a7da8cc2eac55db5c4688cb2bc483f5e923261fa3a285bf +$(package)_build_subdir:=bridge/cmd +$(package)_dependencies:=native_rust +$(package)_patches:=Cargo.lock cargo-config.toml ../native_rust/fix-elf-interpreter.sh +$(package)_vendored_file_name:=native_cxxbridge-$($(package)_version)-vendored.tar.gz +$(package)_cargo_manifest:=bridge/cmd/Cargo.toml +$(package)_cargo_lock_path:=Cargo.lock + +define $(package)_preprocess_cmds + cp $($(package)_patch_dir)/Cargo.lock . +endef + +define $(package)_build_cmds + $($(package)_cargo) build --locked --release --package=cxxbridge-cmd --bin=cxxbridge +endef + +define $(package)_stage_cmds + $($(package)_cargo) install --locked --path=. --bin=cxxbridge --root=$($(package)_staging_prefix_dir) && \ + mkdir -p $($(package)_staging_prefix_dir)/lib && \ + bash $($(package)_patch_dir)/fix-elf-interpreter.sh \ + $($(package)_staging_prefix_dir)/lib \ + $($(package)_staging_prefix_dir)/bin/cxxbridge +endef diff --git a/depends/packages/native_protobuf.mk b/depends/packages/native_protobuf.mk new file mode 100644 index 000000000000..52c3745ee279 --- /dev/null +++ b/depends/packages/native_protobuf.mk @@ -0,0 +1,43 @@ +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +package=native_protobuf +$(package)_version=32.0 +$(package)_download_path=https://github.com/protocolbuffers/protobuf/releases/download/v$($(package)_version) + +# Linux (ARMv8) +$(package)_file_name_aarch64_linux=protoc-$($(package)_version)-linux-aarch_64.zip +$(package)_sha256_hash_aarch64_linux=56af3fc2e43a0230802e6fadb621d890ba506c5c17a1ae1070f685fe79ba12d0 + +# Linux (x86_64) +$(package)_file_name_x86_64_linux=protoc-$($(package)_version)-linux-x86_64.zip +$(package)_sha256_hash_x86_64_linux=7ca037bfe5e5cabd4255ccd21dd265f79eb82d3c010117994f5dc81d2140ee88 + +# macOS (ARMv8) +$(package)_file_name_aarch64_darwin=protoc-$($(package)_version)-osx-aarch_64.zip +$(package)_sha256_hash_aarch64_darwin=09a2c729cc821215cc0d4c564b761760961fe338c52f24b302fd7e18e7b675d1 + +# macOS (x86_64) +$(package)_file_name_x86_64_darwin=protoc-$($(package)_version)-osx-x86_64.zip +$(package)_sha256_hash_x86_64_darwin=63eeba15ddc12ab11b0a8bce81fb2d46cc69022c3e6ad21fecde90d52139bff6 + +$(package)_file_name=$($(package)_file_name_$(build_arch)_$(build_os)) +$(package)_sha256_hash=$($(package)_sha256_hash_$(build_arch)_$(build_os)) + +ifeq ($($(package)_file_name),) +$(error native_protobuf has no prebuilt protoc $($(package)_version) for $(build_arch)-$(build_os)) +endif + +define $(package)_extract_cmds + echo "$($(package)_sha256_hash) $($(package)_source)" > .$($(package)_file_name).hash && \ + $(build_SHA256SUM) -c .$($(package)_file_name).hash && \ + python3 -m zipfile -e $($(package)_source) . +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_prefix_dir)/bin $($(package)_staging_prefix_dir)/include && \ + cp bin/protoc $($(package)_staging_prefix_dir)/bin/ && \ + chmod 0755 $($(package)_staging_prefix_dir)/bin/protoc && \ + cp -R include/google $($(package)_staging_prefix_dir)/include/ +endef diff --git a/depends/packages/native_rust.mk b/depends/packages/native_rust.mk new file mode 100644 index 000000000000..b474a0971ae3 --- /dev/null +++ b/depends/packages/native_rust.mk @@ -0,0 +1,55 @@ +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# To update the Rust compiler, change the version below and then run the script +# ./contrib/devtools/update-rust-hashes.py + +package:=native_rust +$(package)_version:=1.92.0 +$(package)_download_path:=https://static.rust-lang.org/dist +$(package)_patches:=fix-elf-interpreter.sh + +# Linux (ARMv8) +$(package)_file_name_aarch64_linux:=rust-$($(package)_version)-aarch64-unknown-linux-gnu.tar.gz +$(package)_sha256_hash_aarch64_linux:=c812028423c3d7dd7ba99f66101e9e1aa3f66eab44a1285f41c363825d49dca4 + +# Linux (x86_64) +$(package)_file_name_x86_64_linux:=rust-$($(package)_version)-x86_64-unknown-linux-gnu.tar.gz +$(package)_sha256_hash_x86_64_linux:=6e5efd6c25953b2732d4e6b1842512536650c68cf72a8b99a0fc566012dd6ca5 + +# macOS (ARMv8) +$(package)_file_name_aarch64_darwin:=rust-$($(package)_version)-aarch64-apple-darwin.tar.gz +$(package)_sha256_hash_aarch64_darwin:=235a6cca2dd4881130a9ae61ad1149bbf28bba184dd4621700f0c98c97457716 + +# macOS (x86_64) +$(package)_file_name_x86_64_darwin:=rust-$($(package)_version)-x86_64-apple-darwin.tar.gz +$(package)_sha256_hash_x86_64_darwin:=fc6868991e61e9262272effbb8956b23428430f5f4300c1b48eaae3969f8af2a + +$(package)_file_name=$($(package)_file_name_$(build_arch)_$(build_os)) +$(package)_sha256_hash=$($(package)_sha256_hash_$(build_arch)_$(build_os)) + +define $(package)_set_vars +$(package)_stage_opts=--disable-ldconfig +$(package)_stage_build_opts=--without=rust-docs-json-preview,rust-docs +endef + +define $(package)_fetch_cmds +$(call fetch_file,$(package),$($(package)_download_path),$($(package)_file_name),$($(package)_file_name),$($(package)_sha256_hash)) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)/$(host_prefix)/native/bin && \ + mkdir -p $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib && \ + cp cargo/bin/cargo $($(package)_staging_dir)/$(host_prefix)/native/bin/ && \ + cp rustc/bin/rustc $($(package)_staging_dir)/$(host_prefix)/native/bin/ && \ + cp rustc/bin/rustdoc $($(package)_staging_dir)/$(host_prefix)/native/bin/ && \ + cp -r rustc/lib/* $($(package)_staging_dir)/$(host_prefix)/native/lib/ && \ + cp -r rust-std-*/lib/rustlib/* $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib/ && \ + bash $($(package)_patch_dir)/fix-elf-interpreter.sh \ + $($(package)_staging_dir)/$(host_prefix)/native/lib \ + $($(package)_staging_dir)/$(host_prefix)/native/bin/cargo \ + $($(package)_staging_dir)/$(host_prefix)/native/bin/rustc \ + $($(package)_staging_dir)/$(host_prefix)/native/bin/rustdoc +endef diff --git a/depends/packages/packages.mk b/depends/packages/packages.mk index 7e0bb2633219..a8630c462704 100644 --- a/depends/packages/packages.mk +++ b/depends/packages/packages.mk @@ -2,6 +2,9 @@ packages:=gmp backtrace boost_packages = boost +rust_packages = rustcxx rust_stdlib +rust_native_packages = native_rust native_cxxbridge + libevent_packages = libevent qrencode_linux_packages = qrencode @@ -26,4 +29,7 @@ natpmp_packages=libnatpmp multiprocess_packages = libmultiprocess capnp multiprocess_native_packages = native_libmultiprocess native_capnp +platform_packages = mbedtls rust_stdlib tenderdash_sources platform_cxx +platform_native_packages = native_protobuf native_rust + usdt_linux_packages=systemtap diff --git a/depends/packages/platform_cxx.mk b/depends/packages/platform_cxx.mk new file mode 100644 index 000000000000..5a959fe2e350 --- /dev/null +++ b/depends/packages/platform_cxx.mk @@ -0,0 +1,37 @@ +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +package=platform_cxx +$(package)_version=df4fdb68559ef57d50624b7f0841594aef8647e5 +$(package)_download_path=https://github.com/dashpay/platform/archive +$(package)_download_file=$($(package)_version).tar.gz +$(package)_file_name=platform-$($(package)_version).tar.gz +$(package)_sha256_hash=935b64a4f3acf48840706d573acc4c43ce4ac44272265ea96af45e35c47d829a +$(package)_build_subdir=packages/rs-platform-cxx/standalone +$(package)_dependencies=native_rust rust_stdlib native_protobuf tenderdash_sources +$(package)_patches=cargo-config.toml +$(package)_vendored_file_name=platform-cxx-$($(package)_version)-vendored.tar.gz +$(package)_cargo_manifest=packages/rs-platform-cxx/standalone/Cargo.toml +$(package)_cargo_lock_path=packages/rs-platform-cxx/standalone/Cargo.lock + +define $(package)_preprocess_cmds + true +endef + +define $(package)_build_cmds + mkdir -p target && \ + cp $(host_prefix)/tenderdash-sources/tenderdash-*.zip target/ && \ + CARGO_BUILD_TARGET=$(rust_stdlib_target) \ + CARGO_TARGET_DIR=$($(package)_build_dir)/target \ + PROTOC=$(build_prefix)/bin/protoc \ + PROTOC_INCLUDE=$(build_prefix)/include \ + $($(package)_cargo) build --locked --offline --release --target $(rust_stdlib_target) +endef + +define $(package)_stage_cmds + CARGO_BUILD_TARGET=$(rust_stdlib_target) \ + CARGO_PROFILE=release \ + CARGO_TARGET_DIR=$($(package)_build_dir)/target \ + bash ../install.sh $($(package)_staging_prefix_dir) +endef diff --git a/depends/packages/rust_stdlib.mk b/depends/packages/rust_stdlib.mk new file mode 100644 index 000000000000..99395326ebad --- /dev/null +++ b/depends/packages/rust_stdlib.mk @@ -0,0 +1,70 @@ +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# This version is synchronized from native_rust.mk by update-rust-hashes.py. + +package:=rust_stdlib +$(package)_version:=1.92.0 +$(package)_download_path:=https://static.rust-lang.org/dist +$(package)_dependencies:=native_rust + +# Rust support is deliberately confined to the hosts we actually validate +# (the Guix release set plus native development hosts). RUST=1 on any other +# host fails explicitly below rather than fetching a stdlib we never test. + +# Linux (ARMv8) +$(package)_targets += aarch64-unknown-linux-musl +$(package)_target_aarch64-unknown-linux-gnu:=aarch64-unknown-linux-musl +$(package)_sha256_hash_aarch64-unknown-linux-musl:=715fbcfd8712c723947a020d0371c8a1a21f7531f2b696aeaed50ac23ba675c9 + +# Linux (RISCV64GC) +$(package)_targets += riscv64gc-unknown-linux-musl +$(package)_target_riscv64-unknown-linux-gnu:=riscv64gc-unknown-linux-musl +$(package)_target_riscv64gc-unknown-linux-gnu:=riscv64gc-unknown-linux-musl +$(package)_sha256_hash_riscv64gc-unknown-linux-musl:=34f5722ff2a0940bcd7ff6603a7748d2b963de72f6f713579c39c74ead06a7a0 + +# Linux (x86_64) +$(package)_targets += x86_64-unknown-linux-musl +$(package)_target_x86_64-unknown-linux-gnu:=x86_64-unknown-linux-musl +$(package)_sha256_hash_x86_64-unknown-linux-musl:=8bfd9a42c8295949d556587201acdb35d2bfb8b7ce55223845f337aa5614f9a3 + +# macOS (ARMv8) +$(package)_targets += aarch64-apple-darwin +$(package)_target_aarch64-apple-darwin:=aarch64-apple-darwin +$(package)_target_arm64-apple-darwin:=aarch64-apple-darwin +$(package)_sha256_hash_aarch64-apple-darwin:=b1f55aac4bc982ea67b68b262b711263005e470d31cab5d09d534bc1866d455a + +# macOS (x86_64) +$(package)_targets += x86_64-apple-darwin +$(package)_target_x86_64-apple-darwin:=x86_64-apple-darwin +$(package)_sha256_hash_x86_64-apple-darwin:=1e5a8fee4e038ea2d35d82a680e2b9bf44ffccb3746aaf9dbdc56cb14152dcb8 + +# Windows (x86_64) +$(package)_targets += x86_64-pc-windows-gnu +$(package)_target_x86_64-w64-mingw32:=x86_64-pc-windows-gnu +$(package)_sha256_hash_x86_64-pc-windows-gnu:=6256f3497e3b14b6650511e84fdfb51fc632db1908ae5a173dffcdc96c80b7ce + +$(package)_target:=$(or \ + $($(package)_target_$(canonical_host)),\ + $($(package)_target_$(subst -pc-,-unknown-,$(canonical_host))),\ + $($(package)_target_$(subst -unknown-,-pc-,$(canonical_host))),\ + $($(package)_target_$(subst -linux-,-unknown-linux-,$(canonical_host))),\ + $(if $(findstring -apple-darwin,$(canonical_host)),$(host_arch)-apple-darwin)) + +ifeq ($($(package)_target),) +$(error Unsupported Rust standard library target: $(canonical_host)) +endif + +$(package)_file_name=rust-std-$($(package)_version)-$($(package)_target).tar.gz +$(package)_sha256_hash=$($(package)_sha256_hash_$($(package)_target)) + +define $(package)_fetch_cmds + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_file_name),$($(package)_file_name),$($(package)_sha256_hash)) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib && \ + cp -r rust-std-$($(package)_target)/lib/rustlib/$($(package)_target) $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib/ +endef diff --git a/depends/packages/rustcxx.mk b/depends/packages/rustcxx.mk new file mode 100644 index 000000000000..13125507f290 --- /dev/null +++ b/depends/packages/rustcxx.mk @@ -0,0 +1,17 @@ +# Copyright (c) 2022-2023 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +package:=rustcxx +$(package)_version:=$(native_cxxbridge_version) +$(package)_file_name:=$(native_cxxbridge_file_name) +$(package)_sha256_hash:=$(native_cxxbridge_sha256_hash) + +define $(package)_fetch_cmds + $(call native_cxxbridge_fetch_cmds,native_cxxbridge) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_prefix_dir)/include/rust && \ + cp include/cxx.h $($(package)_staging_prefix_dir)/include/rust +endef diff --git a/depends/packages/tenderdash_sources.mk b/depends/packages/tenderdash_sources.mk new file mode 100644 index 000000000000..057abbf6048b --- /dev/null +++ b/depends/packages/tenderdash_sources.mk @@ -0,0 +1,36 @@ +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# Source-only package: the tenderdash source archive that tenderdash-proto's +# build.rs (rs-tenderdash-abci, a dependency of the Platform CXX package) +# compiles its protobuf definitions from. +# Online builds download this zip themselves at cargo build time; depends +# builds must not touch the network, so the sha256-pinned archive is staged +# verbatim into the prefix and platform_cxx copies it into the Cargo target +# directory, where build.rs treats it as a pre-populated download +# cache (cache file name: tenderdash-$(TENDERDASH_COMMITISH).zip). +# +# The version must match TENDERDASH_COMMITISH default of the pinned +# tenderdash-proto crate (rs-tenderdash-abci proto/build.rs). + +package=tenderdash_sources +$(package)_version=1.5.1 +$(package)_download_path=https://github.com/dashpay/tenderdash/archive +$(package)_download_file=v$($(package)_version).zip +$(package)_file_name=tenderdash-v$($(package)_version).zip +$(package)_sha256_hash=7a8844899a4635a6c2f55057e0c0f7cec357907d0cfeb2900e035760cf187f9a + +# Keep the archive as-is: the consumer (tenderdash-proto build.rs) unzips it +# from its own cache directory, so extraction here would only be discarded. +define $(package)_extract_cmds + mkdir -p $($(package)_extract_dir) && \ + echo "$($(package)_sha256_hash) $($(package)_source)" > $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + $(build_SHA256SUM) -c $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + cp $($(package)_source) $($(package)_file_name) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)/$(host_prefix)/tenderdash-sources && \ + cp $($(package)_file_name) $($(package)_staging_dir)/$(host_prefix)/tenderdash-sources/ +endef diff --git a/depends/patches/native_cxxbridge/Cargo.lock b/depends/patches/native_cxxbridge/Cargo.lock new file mode 100644 index 000000000000..91f145808c7f --- /dev/null +++ b/depends/patches/native_cxxbridge/Cargo.lock @@ -0,0 +1,568 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.198" +dependencies = [ + "cc", + "cxx-build", + "cxx-gen", + "cxx-test-suite", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "indoc", + "link-cplusplus", + "proc-macro2", + "quote", + "rustversion", + "scratch", + "target-triple", + "tempfile", + "trybuild", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +dependencies = [ + "cc", + "codespan-reporting", + "cxx", + "cxx-gen", + "indexmap", + "pkg-config", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxx-gen" +version = "0.7.198" +dependencies = [ + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxx-test-suite" +version = "0.0.0" +dependencies = [ + "cxx", + "cxx-build", + "cxxbridge-flags", + "serde", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +dependencies = [ + "cxx", + "indexmap", + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "demo" +version = "0.0.0" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "trybuild" +version = "1.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c5c9f7b7b1a048dd2bebdb7260b0cc71ec5587b19352fa5c3cd9e1c067103f0" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/depends/patches/native_cxxbridge/cargo-config.toml b/depends/patches/native_cxxbridge/cargo-config.toml new file mode 100644 index 000000000000..01ab67f3bd1f --- /dev/null +++ b/depends/patches/native_cxxbridge/cargo-config.toml @@ -0,0 +1,5 @@ +[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "vendored" diff --git a/depends/patches/native_rust/fix-elf-interpreter.sh b/depends/patches/native_rust/fix-elf-interpreter.sh new file mode 100755 index 000000000000..ccf05f8ecc05 --- /dev/null +++ b/depends/patches/native_rust/fix-elf-interpreter.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +export LC_ALL=C + +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +LIBDIR="$1" +shift + +if ! command -v patchelf >/dev/null 2>&1; then + # Inside a Guix environment the prebuilt binaries cannot run without + # having their interpreter patched, so a missing patchelf is fatal there. + case "$(command -v ls)" in + /gnu/store/*) + echo "ERROR: patchelf is required inside the Guix environment but was not found" >&2 + exit 1 + ;; + esac + echo "patchelf not found, skipping ELF fix" + exit 0 +fi + +# Get the interpreter from a known working binary (ls) +LS_PATH=$(command -v ls) +GUIX_INTERP=$(patchelf --print-interpreter "$LS_PATH" 2>/dev/null) + +if [ -z "$GUIX_INTERP" ]; then + echo "Could not detect interpreter, skipping" + exit 0 +fi + +echo "Detected interpreter: $GUIX_INTERP" + +# Find and copy runtime libraries the prebuilt binaries need into our lib +# directory so the $ORIGIN-based RPATH can resolve them. +for libname in libgcc_s.so.1 libz.so.1; do + LIB_SRC="" + + # Method 1: Use gcc to find it + if command -v gcc >/dev/null 2>&1; then + CANDIDATE=$(gcc -print-file-name="$libname" 2>/dev/null) + if [ -f "$CANDIDATE" ]; then + LIB_SRC="$CANDIDATE" + else + GCC_PATH=$(command -v gcc) + GCC_PREFIX=$(dirname "$(dirname "$GCC_PATH")") + if [ -f "$GCC_PREFIX/lib/$libname" ]; then + LIB_SRC="$GCC_PREFIX/lib/$libname" + fi + fi + fi + + # Method 2: Search LIBRARY_PATH + if [ -z "$LIB_SRC" ] && [ -n "$LIBRARY_PATH" ]; then + IFS=':' read -ra LIB_PATHS <<< "$LIBRARY_PATH" + for libpath in "${LIB_PATHS[@]}"; do + if [ -f "$libpath/$libname" ]; then + LIB_SRC="$libpath/$libname" + break + fi + done + fi + + if [ -n "$LIB_SRC" ]; then + # Resolve symlinks and copy the actual file + LIB_REAL=$(readlink -f "$LIB_SRC") + echo "Copying $libname from: $LIB_REAL" + cp "$LIB_REAL" "$LIBDIR/$libname" + else + echo "WARNING: Could not find $libname to copy" + fi +done + +# RPATH just needs $ORIGIN/../lib - everything is self-contained +GUIX_RPATH="\$ORIGIN/../lib" +echo "Using RPATH: $GUIX_RPATH" + +for binary in "$@"; do + if [ -f "$binary" ]; then + echo "Patching: $binary" + patchelf --set-interpreter "$GUIX_INTERP" "$binary" + patchelf --set-rpath "$GUIX_RPATH" "$binary" + fi +done + +if [ -n "$1" ]; then + echo "Verifying first binary:" + patchelf --print-interpreter "$1" + patchelf --print-rpath "$1" +fi diff --git a/depends/patches/platform_cxx/cargo-config.toml b/depends/patches/platform_cxx/cargo-config.toml new file mode 100644 index 000000000000..1f46305c8c3f --- /dev/null +++ b/depends/patches/platform_cxx/cargo-config.toml @@ -0,0 +1,30 @@ +[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "vendored" + +[source."git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900"] +git = "https://github.com/dashpay/agora-blsful" +rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9"] +git = "https://github.com/dashpay/grovedb" +rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.1"] +git = "https://github.com/dashpay/rs-tenderdash-abci" +tag = "v1.5.1" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/rust-dashcore?rev=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb"] +git = "https://github.com/dashpay/rust-dashcore" +rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/vsss-rs?branch=main"] +git = "https://github.com/dashpay/vsss-rs" +branch = "main" +replace-with = "vendored-sources" diff --git a/doc/README.md b/doc/README.md index 384ba6442f66..585f9ba2b4a8 100644 --- a/doc/README.md +++ b/doc/README.md @@ -34,6 +34,7 @@ Building The following are developer notes on how to build Dash Core on your native platform. They are not complete guides, but include notes on the necessary libraries, compile flags, etc. - [Dependencies](dependencies.md) +- [Rust](rust.md) - [macOS Build Notes](build-osx.md) - [Unix Build Notes](build-unix.md) - [Windows Build Notes](build-windows.md) diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index 74aff7d392f8..9846f7f26a0b 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -3,9 +3,9 @@ Assumeutxo is a feature that allows fast bootstrapping of a validating dashd instance with a very similar security model to assumevalid. -The RPC commands `dumptxoutset` and `loadtxoutset` are used to respectively generate -and load UTXO snapshots. The utility script `./contrib/devtools/utxo_snapshot.sh` may -be of use. +The RPC commands `dumptxoutset` and `loadtxoutset` (yet to be merged) are used to +respectively generate and load UTXO snapshots. The utility script +`./contrib/devtools/utxo_snapshot.sh` may be of use. ## General background @@ -17,14 +17,9 @@ be of use. - A new block index `nStatus` flag is introduced, `BLOCK_ASSUMED_VALID`, to mark block index entries that are required to be assumed-valid by a chainstate created - from a UTXO snapshot. This flag is mostly used as a way to modify certain + from a UTXO snapshot. This flag is used as a way to modify certain CheckBlockIndex() logic to account for index entries that are pending validation by a - chainstate running asynchronously in the background. We also use this flag to control - which index entries are added to setBlockIndexCandidates during LoadBlockIndex(). - -- Indexing implementations via BaseIndex can no longer assume that indexation happens - sequentially, since background validation chainstates can submit BlockConnected - events out of order with the active chain. + chainstate running asynchronously in the background. - The concept of UTXO snapshots is treated as an implementation detail that lives behind the ChainstateManager interface. The external presentation of the changes @@ -76,9 +71,15 @@ original chainstate remains in use as active. Once the snapshot chainstate is loaded and validated, it is promoted to active chainstate and a sync to tip begins. A new chainstate directory is created in the -datadir for the snapshot chainstate called `chainstate_snapshot`. When this directory -is present in the datadir, the snapshot chainstate will be detected and loaded as -active on node startup (via `DetectSnapshotChainstate()`). +datadir for the snapshot chainstate called `chainstate_snapshot`. + +When this directory is present in the datadir, the snapshot chainstate will be detected +and loaded as active on node startup (via `DetectSnapshotChainstate()`). + +A special file is created within that directory, `base_blockhash`, which contains the +serialized `uint256` of the base block of the snapshot. This is used to reinitialize +the snapshot chainstate on subsequent inits. Otherwise, the directory is a normal +leveldb database. | | | | ---------- | ----------- | @@ -88,7 +89,7 @@ active on node startup (via `DetectSnapshotChainstate()`). The snapshot begins to sync to tip from its base block, technically in parallel with the original chainstate, but it is given priority during block download and is allocated most of the cache (see `MaybeRebalanceCaches()` and usages) as our chief -consideration is getting to network tip. +goal is getting to network tip. **Failure consideration:** if shutdown happens at any point during this phase, both chainstates will be detected during the next init and the process will resume. @@ -107,33 +108,36 @@ sequentially. ### Background chainstate hits snapshot base block Once the tip of the background chainstate hits the base block of the snapshot -chainstate, we stop use of the background chainstate by setting `m_stop_use` (not yet -committed - see bitcoin#15606), in `CompleteSnapshotValidation()`, which is checked in -`ActivateBestChain()`). We hash the background chainstate's UTXO set contents and -ensure it matches the compiled value in `CMainParams::m_assumeutxo_data`. - -The background chainstate data lingers on disk until shutdown, when in -`ChainstateManager::Reset()`, the background chainstate is cleaned up with -`ValidatedSnapshotShutdownCleanup()`, which renames the `chainstate_[hash]` datadir as -`chainstate`. +chainstate, we stop use of the background chainstate by setting `m_disabled`, in +`MaybeCompleteSnapshotValidation()`, which is checked in `ActivateBestChain()`. We hash the +background chainstate's UTXO set contents and ensure it matches the compiled value in +`CMainParams::m_assumeutxo_data`. In Dash, completion additionally compares the +deterministic masternode-list hash the background chainstate derived at the base block +against the hash recorded at snapshot activation, and the EvoDB best-block markers +against both chainstates' coins tips; any divergence fails completion with +`EVO_STATE_MISMATCH` and quarantines the snapshot exactly like a UTXO hash mismatch. | | | | ---------- | ----------- | -| number of chainstates | 2 (ibd has `m_stop_use=true`) | +| number of chainstates | 2 (ibd has `m_disabled=true`) | | active chainstate | snapshot | -**Failure consideration:** if dashd unexpectedly halts after `m_stop_use` is set on -the background chainstate but before `CompleteSnapshotValidation()` can finish, the -need to complete snapshot validation will be detected on subsequent init by -`ChainstateManager::CheckForUncleanShutdown()`. +The background chainstate data lingers on disk until the program is restarted. ### Dashd restarts sometime after snapshot validation has completed -When dashd initializes again, what began as the snapshot chainstate is now -indistinguishable from a chainstate that has been built from the traditional IBD -process, and will be initialized as such. +After a shutdown and subsequent restart, `LoadChainstate()` cleans up the background +chainstate with `ValidatedSnapshotCleanup()`, which renames the `chainstate_snapshot` +datadir as `chainstate` and removes the now unnecessary background chainstate data. | | | | ---------- | ----------- | | number of chainstates | 1 | -| active chainstate | ibd | +| active chainstate | ibd (was snapshot, but is now fully validated) | + +What began as the snapshot chainstate is now indistinguishable from a chainstate that +has been built from the traditional IBD process, and will be initialized as such. + +A file will be left in `chainstate/base_blockhash`, which indicates that the +chainstate, even though now fully validated, was originally started from a snapshot +with the corresponding base blockhash. diff --git a/doc/design/platform-rust-scope.md b/doc/design/platform-rust-scope.md new file mode 100644 index 000000000000..749a8290fc67 --- /dev/null +++ b/doc/design/platform-rust-scope.md @@ -0,0 +1,216 @@ +# Platform-GUI on Real Rust Crates — Scope Investigation + +Decision document for the `platform-gui-rust` track: re-implement the +internals of PR PastaPastaPasta/dash#49's `--enable-platform-gui` feature on +top of the real Dash Platform Rust crates, built and linked only when the flag +is enabled. The investigation below predates the final ownership decision: +the Rust implementation and CXX schema now live in dashpay/platform, while +Dash Core consumes the installed headers and static archive through depends. + +Three candidate scopes were spiked and measured. Spike workspaces (with +committed lockfiles, test harnesses, and raw logs) live outside the tree in +`~/workspace/platform-rust-spikes/`; every number below states the command +that produced it. + +## The three options + +| | Rust replaces | C++ keeps | +|---|---|---| +| **Option 1 — crypto core** | GroveDB proof verification, drive query verification, DPP (identity/document decode, state-transition build+sign, bincode) | DAPI transport (gRPC-Web/TLS), protobuf codecs, quorum BLS sig check, GUI, wallet | +| **Option 2 — middle** | Option 1 + typed response verification (`drive-proof-verifier` + `dapi-grpc`) | transport, quorum sig check, GUI, wallet | +| **Option 3 — full client** | Everything incl. transport (`dash-sdk` / `rs-dapi-client`, tokio runtime in dash-qt) | GUI, wallet seams | + +Pins used (current latest release): platform `v4.1.0` (MSRV **1.92**), +grovedb `v5.0.1` and rust-dashcore `70d4bf8e` (platform's own pins). + +## Measurements + +Machine: Apple Silicon (aarch64-apple-darwin), rustc 1.92.0. Release profile +mirrors the rust_build branch: `lto = true`, `codegen-units = 1`, +`panic = "abort"`. + +| Metric | Option 1 | Option 2 | Option 3 | Command | +|---|---|---|---|---| +| Lockfile crates | **284** | 408 | 471 | `grep -c '^name = ' Cargo.lock` | +| Build-graph crates (no dev-deps) | ~225 | 344 | 402 | `cargo tree -e no-dev … \| sort -u \| wc -l` | +| Git sources (each needs a vendor config stanza) | **5** | 6 | 7 | `grep -c '^\[source."git' ` | +| Vendor dir / tarball | **205 MB / 33 MB** | 364 MB / 53 MB | 411 MB / 58 MB | `cargo vendor --locked` + `tar czf` | +| Cold release build | **68 s** | 77 s | 196 s | `/usr/bin/time cargo build --release --locked --offline` | +| Warm rebuild (touch lib.rs) | 8.6 s | 8.6 s | 43 s | same | +| Staticlib size | 8.8 MB | 9.4 MB | 17.1 MB | `ls -la target/release/*.a` | +| Stripped C++-binary delta | **~2.5 MB** | ~2.4 MB¹ | **~7.5 MB** | clang++ main + cxx glue vs empty main, stripped | +| `panic=unwind` cost (opt 1) | +3.6 % `.a`, ≈same time | — | — | `release-unwind` profile | +| tokio / hyper / rustls / ring / tonic | **absent** | compiled in² | active | lockfile presence | +| rocksdb / storage | absent | absent | absent | lockfile presence | +| Extra link inputs | none | none | macOS: `Security`, `CoreFoundation`, `SystemConfiguration` frameworks | link test | + +¹ Slight underestimate relative to option 1: the spike bridge references +less of dpp. Both round to "≈2.5 MB". +² Present in the dependency graph and compiled, but fat LTO strips the +unreferenced transport out of the final binary; the cost is vendor surface, +build time, and audit scope — not shipped bytes. + +### Functional proofs (the important part) + +All harnesses live in the spike workspaces and run in CI-able form +(`cargo test`): + +- **GroveDB layer is a drop-in replacement.** All 16 of PR 49's + Rust-generated grovedb proof vectors (`src/test/data/platform/ + grovedb_proof_vectors.json` — keys, ranges, subqueries, conditional + branches, limits, absence, references, sum trees) verify **bit-identically** + (root hash + every result element) through `grovedb v5.0.1` + (`verify` feature, storage-free). `tests/vectors.rs`. +- **Error paths don't panic.** One corrupted byte mid-proof across all 16 + vectors: 7 return `Err`, 9 replay to a wrong root (which the C++ quorum-sig + check rejects), 0 panics/aborts — safe even under `panic=abort`; with + `panic=unwind` (+3.6 % size) cxx additionally converts any residual panic + into a catchable C++ exception. A junk proof fed from C++ through the cxx + bridge produces a clean `std::exception`. +- **Drive layer verified, cross-version stable.** PR 49's drive query vectors + were generated by platform **v4.0.0**; `drive v4.1.0` + (`Drive::verify_identity_balance_for_identity_id`, + `verify_identity_id_by_unique_public_key_hash`) verifies them with matching + roots and values. `tests/drive_vectors.rs`. +- **Signing seam works without exposing wallet keys.** dpp v4.1.0's `Signer` + trait is **async**; driven with `futures::executor::block_on` (no tokio) and + implemented over an external callback (the future cxx call into + `interfaces::Wallet::signPlatformDigest`), both of PR 49's hardest state + transitions build and sign: a DPNS-preorder batch transition (266 bytes) and + an **IdentityCreate** (249 bytes) via + `try_from_identity_with_signer_and_private_key` — HD identity keys stay + behind the callback; only the throwaway one-time asset-lock key crosses as + bytes, matching PR 49's trust model. (The no-raw-key two-signer variant + exists but is coupled to rust-dashcore's `key-wallet`; not worth it.) + `tests/signing.rs`. +- **Option 3 transport works but changes the process model.** A + C++-instantiated tokio runtime fetched an identity from live testnet DAPI + over TLS and proceeded to proof verification, stopping exactly at the + stub `ContextProvider` (the SDK's quorum-key lookup — same shape as PR 49's + `QuorumKeyLookup`, would be backed by local LLMQ data). Requires the three + macOS frameworks at link time and moves quorum-sig verification from + dashbls into Rust (`blsful`). +- **Cross-compile smoke passes** (option 1): `cargo check --target + x86_64-pc-windows-gnu` (needs a mingw C compiler for `blst`) and + `--target x86_64-unknown-linux-musl` both clean on 1.92. + +### Build-infrastructure findings (apply to every option) + +These verify and extend the known dashpay/dash#7109 gaps: + +1. **Git-dependency vendoring works and the fix is mechanical.** + crates.io is unusable (`drive` 0.0.1 is a 2015 name-squat; `dpp`/`grovedb` + are stale), so git deps are unavoidable. `cargo vendor` handles them but + emits per-git-source `[source."git+…"] replace-with` stanzas that 7109's + offline config generation (a single `directory =` line) must learn to + carry. Captured verbatim in the spike results; an offline rebuild from the + vendor dir with a **fresh `CARGO_HOME`** succeeds. +2. **Source-unification patch required.** `grovedb-version` wants + `versioned-feature-core` from crates.io while `platform-version` wants the + same version from git; `cargo vendor` hard-fails on the duplicate. Fixed + with a two-line `[patch."https://github.com/dashpay/versioned-feature-core"]` + redirect. Upstream-worthy. +3. **Floating branch pin upstream.** `vsss-rs` (via `agora-blsful`) is pinned + `branch = "main"`. `--locked` freezes the rev in Cargo.lock, but any + re-lock silently moves it. Should be reported/fixed upstream. +4. **7109's flat `ar -x` aggregation is unusable at this scale — measured.** + 373 of 374 archive member basenames collide between two crate staticlibs + sharing a dependency tree; the flat-extract merge silently drops objects. + The umbrella-crate rework (one workspace staticlib depending on bridge + crates as rlibs, cargo features for conditionality) is mandatory, not + optional. +5. **MSRV.** Nine platform crates declare `rust-version = 1.92`; rustc 1.85.1 + (7109's pin) refuses them by name. Toolchain bump required, as planned. +6. **License / advisory audit** (`cargo deny`, `cargo audit`): everything in + the permissive set except `hex_lit` 0.1.1 (MITNFA — MIT with a + no-false-attribution clause; acceptable), `dpp` has **no license field** + in its Cargo.toml (upstream metadata bug; the repo is MIT), and bincode + 2.0.1 carries RUSTSEC-2025-0141 "unmaintained" (informational — bincode is + platform's wire format either way). +7. **Duplicate crypto implementations enter dash-qt** with any option: + rust-dashcore vendors its own secp256k1 (C), and `blsful`/`blst` sit + beside dashbls. Client-side only, never consensus-facing, but it is review + surface to acknowledge. +8. **dapi-grpc cannot shed tonic's transport by feature** — it is a + target-scoped (`cfg(not(wasm32))`) unconditional dependency with + `transport`+TLS features. Making option 2 lean would require an upstream + "types-only" feature split in dashpay/platform. + +## Comparison + +**Maintenance:** every option pins platform+grovedb+rust-dashcore and re-pins +each platform release (MSRV churn included). Option 1's tree (284 crates) is +~70 % of it shared with options 2/3; the marginal maintenance of 2/3 is the +async/network stack (tokio/hyper/rustls advisories arrive on their own +schedule and would demand pin bumps unrelated to any Dash feature). + +**Security/review:** option 1 compiles zero network-facing Rust; its entire +attack surface is parsers/verifiers fed by C++-fetched bytes — precisely the +code we *want* replaced by the canonical implementation, since the hand-rolled +C++ copies (proof verifier, bincode, DPP layouts, drive tree layout) are where +a silent divergence from mainnet Platform behavior would hurt. Options 2/3 add +~130–190 crates of async runtime + TLS whose main effect is enlarging the +Guix-vendored audit payload (+20–25 MB compressed). Option 3 additionally +moves quorum-sig verification from dashbls (consensus-vetted) into blsful. + +**What C++ gets deleted:** option 1 removes +`src/platform/{proof,dpp}/**`, `drive/{verify,queries}.*` (~3,000 LoC of the +riskiest code) plus the vendored blake3 (~1,780 LoC). Option 2 additionally +deletes `transport/protobuf.*` + response parsing (~400 LoC). Option 3 +additionally deletes `transport/**` (~1,400 LoC of reviewed, working I/O +code) — at the price of an embedded runtime and the biggest tree. + +**Guix/determinism risk:** identical mechanism for all options (vendored +tarball), scaled by size. Option 3 also adds per-platform system-library +coupling (frameworks on macOS; DNS/`getaddrinfo` behavior under the +musl-Rust/glibc-C++ mix on Linux is untested territory). + +## Recommendation: Option 1 (crypto core), leave option 2 as an upgrade path + +Option 1 captures essentially all of the correctness value — canonical +proof verification, canonical DPP encode/sign, canonical drive semantics, +bit-for-bit proven against PR 49's vectors — at the smallest cost in vendor +surface, build time, binary size, and review burden, with **no async runtime +and no network-facing Rust** in dash-qt. The quorum-sig check stays on +dashbls against locally synced LLMQ keys, preserving PR 49's trust +architecture unchanged. + +Option 2's only real prize (deleting the hand-rolled protobuf response +decoding) is blocked from being lean by dapi-grpc's unconditional tonic +transport; if upstream later ships a types-only feature, option 2 becomes a +small incremental PR on top of option 1 — nothing in option 1's design +forecloses it. + +Option 3 is rejected for this track: it discards working, reviewed C++ +transport, triples the binary delta and cold-build time, embeds tokio in a +GUI process, moves BLS verification off dashbls, and maximizes the +Guix/symbol-check/system-library surface — all for functionality PR 49 +already has in C++. + +## Consequences for the implementation + +- The Platform-owned `dash-platform-cxx` package exposes per-query verify functions + (proof bytes + params in → root hash + typed results out), state-transition + build/sign via a cxx-bridged wallet signer (async trait driven by a local + executor; one-time asset-lock key passed as bytes), and identity/document + decoders. Quorum-sig verification, transport, retry/freshness, GUI, and + wallet seams are untouched PR 49 C++. +- Dash Core's depends system pins the Platform source revision and its + standalone lockfile, vendors that dependency closure independently, and + installs `dash/platform/ffi.h`, `dash/platform/signer.h`, generated CXX + headers, and `libdash_platform_cxx.a` into the depends prefix. +- Core owns no Platform Rust crate or generated Platform bridge sources. + Conditional linking keeps the Platform archive out of dashd, dash-cli, + dash-tx, and wallet libraries. +- Core's independent Rust smoke component remains available through + `--enable-rust`, but cannot be linked into the same binaries as the Platform + static archive because both archives contain a Rust and CXX runtime. +- dpp features to pin: `state-transitions`, `state-transition-signing`, + `identity-serialization`, `identity-hashing`, `bls-signatures`, + `dpns-contract`, `dashpay-contract` (the last two also replace PR 49's + hard-coded contract IDs with the canonical definitions). +- PR 49's JSON vectors are kept as cross-implementation regression pins; the + spike proved they pass through the real crates unchanged. +- Report upstream while landing: dpp missing license metadata, vsss-rs + `branch = "main"` pin, dapi-grpc types-only feature request, + versioned-feature-core dual-source. diff --git a/doc/developer-notes.md b/doc/developer-notes.md index 68495d1d10bf..9f26b39d1848 100644 --- a/doc/developer-notes.md +++ b/doc/developer-notes.md @@ -17,6 +17,7 @@ Developer Notes - [Devnet, testnet, and regtest modes](#devnet-testnet-and-regtest-modes) - [DEBUG_LOCKORDER](#debug_lockorder) - [DEBUG_LOCKCONTENTION](#debug_lockcontention) + - [Assertions and Checks](#assertions-and-checks) - [Valgrind suppressions file](#valgrind-suppressions-file) - [Compiling for test coverage](#compiling-for-test-coverage) - [Performance profiling with perf](#performance-profiling-with-perf) @@ -108,6 +109,7 @@ code. - `++i` is preferred over `i++`. - `nullptr` is preferred over `NULL` or `(void*)0`. - `static_assert` is preferred over `assert` where possible. Generally; compile-time checking is preferred over run-time checking. + For run-time checks, see [Assertions and Checks](#assertions-and-checks) on choosing between `assert`/`Assert`, `Assume` and `CHECK_NONFATAL`. - Align pointers and references to the left i.e. use `type& var` and not `type &var`. - Use a named cast or functional cast, not a C-Style cast. When casting between integer types, use functional casts such as `int(x)` or `int{x}` @@ -432,30 +434,73 @@ It can be toggled off again with `dash-cli logging [] '["lock"]'`. ### Assertions and Checks -The util file `src/util/check.h` offers helpers to protect against coding and -internal logic bugs. They must never be used to validate user, network or any -other input. - -* `assert` or `Assert` should be used to document assumptions when any - violation would mean that it is not safe to continue program execution. The - code is always compiled with assertions enabled. - - For example, a nullptr dereference or any other logic bug in validation - code means the program code is faulty and must terminate immediately. -* `CHECK_NONFATAL` should be used for recoverable internal logic bugs. On - failure, it will throw an exception, which can be caught to recover from the - error. - - For example, a nullptr dereference or any other logic bug in RPC code - means that the RPC code is faulty and cannot be executed. However, the - logic bug can be shown to the user and the program can continue to run. -* `Assume` should be used to document assumptions when program execution can - safely continue even if the assumption is violated. In debug builds it - behaves like `Assert`/`assert` to notify developers and testers about - nonfatal errors. In production it doesn't warn or log anything, though the - expression is always evaluated. - - For example it can be assumed that a variable is only initialized once, - but a failed assumption does not result in a fatal bug. A failed - assumption may or may not result in a slightly degraded user experience, - but it is safe to continue program execution. +The util file [`src/util/check.h`](../src/util/check.h) offers helpers to +protect against coding and internal logic bugs. They document invariants the +code itself is responsible for maintaining, and must never be used to validate +user, network, RPC, disk or any other input: untrusted data that does not +match expectations is an ordinary error to be handled, not a bug to be +reported. + +Pick the helper by the cost of continuing with the invariant violated: + +| Cost of continuing | Use | +| --- | --- | +| Undefined behavior, memory corruption, or corrupt persisted/consensus state | `assert` / `Assert` | +| A bug worth investigating, but execution stays well-defined | `Assume` | +| Same, but there is a caller who can be told (RPC/CLI) | `CHECK_NONFATAL` / `NONFATAL_UNREACHABLE` | +| Nothing is broken in-process; the environment failed (disk full, failed DB write) | Not a check: return an error, `AbortNode()`, or `InitError()` | + +**`Assume` is the default.** Reach for `Assert`/`assert` only when you can name +the undefined behavior or corruption that continuing would cause. + +* `Assume` documents "this is how things are supposed to be": a violation means + someone has a bug to chase, but execution stays well-defined. The archetype: + + ```cpp + // Somebody decremented twice if this trips. Nothing is corrupt, but the + // rate limiter is not limiting anything, so it is worth finding out about. + Assume(m_request_count >= 0); + ``` + + A bugged rate limiter may expose us to extra DoS pressure; aborting would + turn that into a guaranteed outage for every user running the release. Never + let an `Assume` be the thing that kills a production node. The expression is + always evaluated, in every build; failures abort only where + `-DABORT_ON_FAILED_ASSUME` is defined — `--enable-debug` and `--enable-fuzz` + builds, i.e. CI's `linux64_multiprocess` and fuzz jobs — and are silent in + release. That coverage is partial: an invariant you actually care about also + needs a test, and code downstream must still cope with the violated case. + +* `assert` / `Assert` is for "continuing is unsafe": a nullptr dereference, + out-of-bounds access, or an invariant whose violation would corrupt data on + disk or consensus state. Aborting must be the safer outcome; such cases + should be rare and obvious to a reader. It is still the right tool where a + precondition genuinely keeps the code below it safe: + `Chainstate::ConnectBlock()` dereferences `pindex` unconditionally, so its + `assert(pindex);` just makes a guaranteed crash diagnosable. Plain `assert()` + is always active — the build never defines `NDEBUG` (`check.h` refuses to + compile with it). `Assert` returns its argument, so a check followed by a + use of the checked value collapses into one expression: + `assert(ptr != nullptr); obj = *ptr;` becomes `obj = *Assert(ptr);` + (`Assume` is an identity function too). Prefer `static_assert` when the + condition is known at compile time. + +* `CHECK_NONFATAL` / `NONFATAL_UNREACHABLE` report internal logic bugs to a + caller: they throw `NonFatalCheckError`, which RPC code catches and turns + into an error message asking the user to file a bug report, and the node + keeps running. Mandatory in RPC code, enforced (best-effort) by + `test/lint/lint-assertions.py` for `src/rpc/` and `src/wallet/rpc*`; use + `NONFATAL_UNREACHABLE()` instead of `assert(false)` there. + +An assertion reachable from P2P messages, RPC arguments, wallet files, or +on-disk data is a remote crash. This cuts especially deep in Dash-specific +code: masternode, LLMQ, InstantSend, ChainLocks, governance and CoinJoin paths +process peer-chosen message contents and read state (EvoDB, quorum caches, DKG +sessions) possibly written by an older or buggy version. There, validate and +reject (misbehaving peer, `state.Invalid(...)`, early return) rather than +assert; use `Assume` for our *own* bookkeeping while still handling the +violated case; and reserve `assert` for the narrow spot where continuing would +corrupt EvoDB, the block index, or the wallet. ### Valgrind suppressions file diff --git a/doc/release-notes-25122.md b/doc/release-notes-25122.md new file mode 100644 index 000000000000..5c5f0f911154 --- /dev/null +++ b/doc/release-notes-25122.md @@ -0,0 +1,7 @@ + +Wallet +------ + +- RPC `getreceivedbylabel` now returns an error, "Label not found + in wallet" (-4), if the label is not in the address book. (dash#7550) + diff --git a/doc/release-notes-25504.md b/doc/release-notes-25504.md new file mode 100644 index 000000000000..bf80f180318a --- /dev/null +++ b/doc/release-notes-25504.md @@ -0,0 +1,6 @@ +Updated RPCs +------------ + +- The `listsinceblock`, `listtransactions` and `gettransaction` output now contain a new + `parent_descs` field for every "receive" entry. +- A new optional `include_change` parameter was added to the `listsinceblock` command. diff --git a/doc/release-notes-7052.md b/doc/release-notes-7052.md new file mode 100644 index 000000000000..fd611b543701 --- /dev/null +++ b/doc/release-notes-7052.md @@ -0,0 +1,45 @@ +P2P and network changes +----------------------- + +- The protocol version was bumped to 70241. The `dsa` message gained a + version-gated flags field declaring which mixing direction a + participant intends. A session commits to carrying promotion/demotion + entries only once a participant is admitted that actually declared one, + and it becomes closed to pre-70241 clients only from that point on; + conversely, a session that has already admitted a pre-70241 client + refuses later promotion/demotion participants. Either way the refusal + happens at acceptance time, before any collateral is committed, so a + client doing ordinary 1:1 mixing is never turned away from a session + simply because of who opened it. Unbalanced (promotion/demotion) DSTXes + are only announced as `dstx` to peers at protocol 70241 or newer, since + older peers would reject them as structurally invalid and penalize the + relayer; those peers are sent a plain `tx` announcement instead, so + they still receive the transaction without the mixing metadata they + cannot parse. (#7052) + +- A mixing session only completes once each side of its denomination is + occupied by nobody or by at least two participants, since coins are + only concealed by other coins of the same size on the same side. A + session that attracts a lone promotion or demotion participant and no + counterpart therefore waits, and expires in the queue stage without + charging anyone's collateral, rather than publishing a transaction that + would identify that participant's coins. Because admission relies on + the declared directions, a participant whose entry deviates from what + it declared has its collateral consumed. (#7052) + +Wallet +------ + +- CoinJoin can now promote and demote between adjacent standard + denominations within a mixing session after V24 activation. + Promotion combines 10 inputs of one denomination into 1 output of the + next larger denomination, while demotion splits 1 input into 10 + outputs of the next smaller denomination. Pre-V24 behavior remains + unchanged. (#7052) + +- Conversions only spend fully-mixed coins, and their outputs start + mixing over from zero rounds. The 10:1 shape of a conversion publicly + clusters one participant's coins even inside a mixing transaction, so + a converted coin is not treated as mixed: it re-enters mixing at its + new denomination and disperses normally, while the histories of the + fully-mixed coins that fed the conversion remain protected. (#7052) diff --git a/doc/release-notes-7595.md b/doc/release-notes-7595.md new file mode 100644 index 000000000000..deedec190141 --- /dev/null +++ b/doc/release-notes-7595.md @@ -0,0 +1,9 @@ +GUI changes +----------- + +- Dash-Qt now labels masternode registration and update transactions in the + transaction history as **Masternode Registration** and **Masternode Update** + instead of generic "Payment to yourself" rows. A new **Masternode** filter + shows only these operations. The amount shown is the transaction's net effect + on your wallet (for example, the network fee on a self-funded registration). + (#7595) diff --git a/doc/release-notes-7600.md b/doc/release-notes-7600.md new file mode 100644 index 000000000000..323608c15252 --- /dev/null +++ b/doc/release-notes-7600.md @@ -0,0 +1,16 @@ +RPC changes +----------- + +- Normal and Evo `protx` registration and maintenance commands now share a + typed provider-transaction implementation with other wallet frontends. RPC + names and successful result formats are unchanged. Fixed: when a wallet + cannot completely sign the inputs it selected (e.g. `protx register_submit` + run in a different wallet than the one that prepared the registration), + the command now fails with a clear wallet error naming the problem instead + of reporting success with a partially signed transaction or attempting a + broadcast that failed mempool acceptance with a bare `-26` error. The + external-signing workflow (`protx register_prepare` followed by + `protx register_submit`) is unchanged. + `protx update_service` on a masternode whose state does not yield a usable + default fee source now returns an explicit "specify feeSourceAddress" + parameter error instead of an internal error. (#7600) diff --git a/doc/rust.md b/doc/rust.md new file mode 100644 index 000000000000..6fd1252365a8 --- /dev/null +++ b/doc/rust.md @@ -0,0 +1,134 @@ +# Rust in Dash Core + +Dash Core has optional support for components written in Rust, bridged into +the C++ codebase with [cxx](https://cxx.rs/). Rust support is **disabled by +default**: a default `./configure && make` performs no Rust work at all and +requires no Rust tooling. + +The Rust code lives in `rust/`. Component crates (currently only `chirp`, a +small smoke-test component) are compiled as dependencies of the umbrella crate +`rust/dashrust`, which cargo builds into a single static library that is +linked into the C++ binaries. The C++ side of each bridge is generated from +the crate's `src/lib.rs` by the `cxxbridge` code generator. + +## Toolchain requirements + +`--enable-rust` requires exact tool versions, enforced at configure time: + +| Tool | Version | +|-------------|------------| +| `rustc` | 1.92.0 | +| `cargo` | 1.92.0 | +| `cxxbridge` | 1.0.198 | + +The version pins live in `configure.ac`, `rust-toolchain.toml` (picked up +automatically by rustup) and the depends packages. To update them, change the +version in `depends/packages/native_rust.mk` and run +`contrib/devtools/update-rust-hashes.py`, and/or change +`depends/packages/native_cxxbridge.mk` and run +`contrib/devtools/update-native-cxxbridge.py`, keeping `configure.ac` in sync. + +## Building with depends (recommended) + +The depends system can provision the whole Rust toolchain, the Rust standard +library for the target, and offline copies of all crate dependencies: + +```bash +make -C depends RUST=1 HOST=x86_64-pc-linux-gnu +./configure --prefix=$(pwd)/depends/x86_64-pc-linux-gnu +make +``` + +`RUST=1` builds/installs into the depends prefix: + +- `native_rust`: the pinned Rust compiler and cargo for the build machine; +- `native_cxxbridge`: the pinned `cxxbridge` code generator; +- `rust_stdlib`: the pre-built Rust standard library for the target triple; +- `rustcxx`: the `rust/cxx.h` header; +- `vendored-sources/`: all crates from the workspace `Cargo.lock`, vendored + for offline use. + +The generated `share/config.site` then makes `./configure` default to +`--enable-rust` with `CARGO`, `RUSTC`, `CXXBRIDGE` and +`RUST_VENDORED_SOURCES` pointing into the depends prefix, so no extra +configure flags are needed. Everything after the depends downloads works +offline. + +For an offline sources mirror, `make -C depends RUST=1 download` additionally +fetches the Rust standard libraries for all supported targets +(`download-rust-std`) and creates a pre-vendored crate archive +(`vendor-crates`) in `SOURCES_PATH`. + +## Supported hosts + +Rust support is deliberately confined to the hosts that are validated in CI +and release builds. `--enable-rust` (and `RUST=1` in depends) fails +explicitly on any other host rather than producing binaries for a target that +is never tested: + +- Linux: `x86_64`, `aarch64`, `riscv64` (glibc and musl; depends builds use + the musl-targeted standard library) +- macOS: `x86_64`, `arm64` +- Windows: `x86_64` (MinGW-w64) + +Android is explicitly unsupported. Additional targets can be added later +together with CI lanes that exercise them. + +## Building with a system toolchain + +Instead of depends, a system Rust toolchain can be used as long as it matches +the pinned versions exactly. With rustup, the pinned toolchain from +`rust-toolchain.toml` is selected automatically; the matching code generator +can be installed with: + +```bash +rustup toolchain install 1.92.0 +cargo install cxxbridge-cmd --version 1.0.198 --locked +``` + +Note that rustup resolves `rust-toolchain.toml` from the current working +directory, so for out-of-tree builds export `RUSTUP_TOOLCHAIN=1.92.0` (the +build system propagates it to all cargo invocations). + +Configure builds are offline by default, so a system-toolchain build must +either provide vendored crates or opt in to network access: + +```bash +# Offline: vendor the workspace dependencies once, then point configure at them +cargo vendor --locked /path/to/vendored-sources +./configure --enable-rust RUST_VENDORED_SOURCES=/path/to/vendored-sources + +# Online: let cargo fetch dependencies from the network (developer convenience) +./configure --enable-rust --enable-online-rust +``` + +In offline mode the build generates a cargo config from +`.cargo/config.toml.offline`, adds the vendored directory as the +`crates-io` replacement, and reconstructs source-replacement stanzas for any +git-sourced crates in `Cargo.lock` via +`contrib/devtools/cargo-vendor-git-sources.sh`. Cargo then runs with +`--locked --offline`. In online mode no config is injected and the +developer's own cargo configuration (e.g. in `~/.cargo`) is left in effect; +cargo still runs with `--locked`. + +Useful configure variables (see `./configure --help`): + +- `RUST_VENDORED_SOURCES`: directory containing vendored crate sources + (required for offline builds outside depends); +- `RUSTFLAGS`: defaults to `-C embed-bitcode=yes -C relocation-model=pic`; +- `CARGO_INCREMENTAL`: defaults to `0`; +- `NATIVE_CC`/`NATIVE_CXX`/`NATIVE_AR`: build-machine tools, required when + cross-compiling (depends sets them automatically). + +`--enable-debug` builds the Rust code with cargo's debug profile instead of +the release profile. + +## Source tarballs + +Source distributions generated from a Rust-enabled tree (`make dist`) ship +the generated C++ bridge sources under `rust//gen/` together with a +stamp recording the `cxxbridge` version that produced them. Builds from such +a tarball reuse the shipped artifacts instead of regenerating them, provided +the artifacts are at least as new as the stamp and strictly newer than the +crate's `lib.rs`; editing `lib.rs` forces regeneration with the pinned +`cxxbridge`. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000000..f19782d3c556 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.92.0" diff --git a/rust/Makefile.am b/rust/Makefile.am new file mode 100644 index 000000000000..0b20548d13a5 --- /dev/null +++ b/rust/Makefile.am @@ -0,0 +1,42 @@ +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +include $(top_srcdir)/rust/Makefile.common.include +include $(top_srcdir)/rust/Makefile.libs.include + +EXTRA_DIST = \ + $(LIBRUSTDEPS_FILES) \ + $(LIBRUST_CHIRP_FILES) + +CXXBRIDGE_TARGETS = cxxbridge-chirp + +if ENABLE_RUST +all-local: $(CARGO_CONFIGURED) cargo-build-dashrust $(CXXBRIDGE_TARGETS) + +cargo-build: $(CARGO_CONFIGURED) cargo-build-dashrust +else +all-local: + +cargo-build: +endif + +cargo-clean: cargo-clean-dashrust + +# Aggregate dist hook: per-crate includes define dist- targets; new +# crates append here instead of defining their own dist-hook (automake allows +# only one dist-hook recipe per Makefile). +if ENABLE_RUST +dist-hook: dist-chirp +endif + +cargo-clean-config: + $(AM_V_at)rm -f $(abs_top_builddir)/.cargo/.configured-for-online + $(AM_V_at)rm -f $(abs_top_builddir)/.cargo/.configured-for-offline + $(AM_V_at)rm -f $(CARGO_BUILD_CONFIG) + $(AM_V_at)rm -f $(RUST_NATIVE_LINKER) $(RUST_TARGET_LINKER) + +clean-local: cargo-clean-dashrust cxxbridge-clean-chirp cargo-clean-config + +.PHONY: cargo-build cargo-clean cargo-clean-config diff --git a/rust/Makefile.chirp.include b/rust/Makefile.chirp.include new file mode 100644 index 000000000000..e11b81459189 --- /dev/null +++ b/rust/Makefile.chirp.include @@ -0,0 +1,94 @@ +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# cxxbridge codegen for the `chirp` component. Compilation happens through +# the umbrella crate `dashrust` (see Makefile.libs.include); this file only +# generates and distributes the C++ side of the bridge. + +LIBRUST_CHIRP_CRATE_DIR = $(top_srcdir)/rust/chirp +LIBRUST_CHIRP_GEN_DIR = $(abs_top_builddir)/rust/chirp/gen +LIBRUST_CHIRP_GEN_SRCDIR = $(abs_top_srcdir)/rust/chirp/gen +LIBRUST_CHIRP_GEN_STAMP = $(LIBRUST_CHIRP_GEN_DIR)/.cxxbridge-@CXXBRIDGE_REQUIRED_VERSION@ +LIBRUST_CHIRP_GEN_SRCSTAMP = $(LIBRUST_CHIRP_GEN_SRCDIR)/.cxxbridge-@CXXBRIDGE_REQUIRED_VERSION@ +LIBRUST_CHIRP_MANIFEST = $(LIBRUST_CHIRP_CRATE_DIR)/Cargo.toml + +LIBRUST_CHIRP_SRCS = \ + $(LIBRUST_CHIRP_CRATE_DIR)/src/lib.rs + +LIBRUST_CHIRP_BUILD = \ + $(LIBRUST_CHIRP_CRATE_DIR)/build.rs + +LIBRUST_CHIRP_CPP = \ + $(LIBRUST_CHIRP_GEN_DIR)/src/lib.cpp + +LIBRUST_CHIRP_H = \ + $(LIBRUST_CHIRP_GEN_DIR)/include/rust/chirp/lib.h + +LIBCXXBRIDGE_H = \ + $(LIBRUST_CHIRP_GEN_DIR)/include/rust/cxx.h + +LIBRUST_CHIRP_INCLUDES = \ + -I$(LIBRUST_CHIRP_GEN_DIR)/include + +LIBRUST_CHIRP_FILES = \ + $(LIBRUST_CHIRP_BUILD) \ + $(LIBRUST_CHIRP_MANIFEST) \ + $(LIBRUST_CHIRP_SRCS) + +$(LIBRUST_CHIRP_SRCS): ; + +# Pre-generated bridge sources shipped in a source distribution are used when +# they are strictly newer than lib.rs (an edited lib.rs must force +# regeneration) and at least as new as the distributed stamp. The stamp +# comparison must accept equal timestamps: ustar archives store mtimes at +# one-second precision, so the stamp and the artifacts it vouches for often +# extract with identical times. + +$(LIBRUST_CHIRP_GEN_STAMP): $(CXXBRIDGE) + $(AM_V_at)$(MKDIR_P) $(@D) + $(AM_V_at)rm -f $(@D)/.cxxbridge-* + $(AM_V_at)touch $@ + +$(LIBRUST_CHIRP_CPP): $(LIBRUST_CHIRP_SRCS) $(LIBRUST_CHIRP_GEN_STAMP) + $(AM_V_at)$(MKDIR_P) $(@D) + $(AM_V_GEN)if test -f $(LIBRUST_CHIRP_GEN_SRCDIR)/src/lib.cpp && \ + test -f $(LIBRUST_CHIRP_GEN_SRCSTAMP) && \ + test $(LIBRUST_CHIRP_GEN_SRCDIR)/src/lib.cpp -nt $< && \ + ! test $(LIBRUST_CHIRP_GEN_SRCSTAMP) -nt $(LIBRUST_CHIRP_GEN_SRCDIR)/src/lib.cpp && \ + test "$(LIBRUST_CHIRP_GEN_SRCDIR)" != "$(LIBRUST_CHIRP_GEN_DIR)"; then \ + cp $(LIBRUST_CHIRP_GEN_SRCDIR)/src/lib.cpp $@; \ + else \ + $(CXXBRIDGE) $< -o $@; \ + fi + +$(LIBRUST_CHIRP_H): $(LIBRUST_CHIRP_SRCS) $(LIBRUST_CHIRP_GEN_STAMP) + $(AM_V_at)$(MKDIR_P) $(@D) + $(AM_V_GEN)if test -f $(LIBRUST_CHIRP_GEN_SRCDIR)/include/rust/chirp/lib.h && \ + test -f $(LIBRUST_CHIRP_GEN_SRCSTAMP) && \ + test $(LIBRUST_CHIRP_GEN_SRCDIR)/include/rust/chirp/lib.h -nt $< && \ + ! test $(LIBRUST_CHIRP_GEN_SRCSTAMP) -nt $(LIBRUST_CHIRP_GEN_SRCDIR)/include/rust/chirp/lib.h && \ + test "$(LIBRUST_CHIRP_GEN_SRCDIR)" != "$(LIBRUST_CHIRP_GEN_DIR)"; then \ + cp $(LIBRUST_CHIRP_GEN_SRCDIR)/include/rust/chirp/lib.h $@; \ + else \ + $(CXXBRIDGE) $< --header -o $@; \ + fi + +$(LIBCXXBRIDGE_H): $(LIBRUST_CHIRP_GEN_STAMP) + $(AM_V_at)$(MKDIR_P) $(@D) + $(AM_V_GEN)$(CXXBRIDGE) --header -o $@ + +cxxbridge-clean-chirp: + $(AM_V_at)rm -rf $(LIBRUST_CHIRP_GEN_DIR) + +cxxbridge-chirp: $(LIBRUST_CHIRP_CPP) $(LIBRUST_CHIRP_H) $(LIBCXXBRIDGE_H) + +dist-chirp: cxxbridge-chirp + $(MKDIR_P) $(distdir)/chirp/gen/src + $(MKDIR_P) $(distdir)/chirp/gen/include/rust/chirp + cp $(LIBRUST_CHIRP_GEN_STAMP) $(distdir)/chirp/gen/ + cp $(LIBRUST_CHIRP_CPP) $(distdir)/chirp/gen/src/ + cp $(LIBRUST_CHIRP_H) $(distdir)/chirp/gen/include/rust/chirp/ + +.PHONY: cxxbridge-chirp cxxbridge-clean-chirp dist-chirp diff --git a/rust/Makefile.common.include b/rust/Makefile.common.include new file mode 100644 index 000000000000..49a937fec265 --- /dev/null +++ b/rust/Makefile.common.include @@ -0,0 +1,85 @@ +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +CARGO_BUILD_OPTS = --locked --target $(RUST_TARGET) +CARGO_BUILD_CONFIG = $(abs_top_builddir)/.cargo/dash-build.toml + +# Host-side helper binaries that cargo compiles and runs (build scripts) link +# against libgcc_s without an RPATH. In hermetic environments such as Guix +# (which sets GUIX_LD_WRAPPER_DISABLE_RPATH and whose loader has no default +# search path providing libgcc_s), they can only resolve it via +# LD_LIBRARY_PATH, so point that at the rustc sysroot lib directory where the +# depends-provided toolchain stages libgcc_s.so.1 and libz.so.1. +RUST_NATIVE_SYSROOT = $(shell "$(RUSTC)" --print sysroot 2>/dev/null) + +CARGO_ENV = \ + $(if $(RUST_NATIVE_SYSROOT),LD_LIBRARY_PATH="$(RUST_NATIVE_SYSROOT)/lib$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}}") \ + MACOSX_DEPLOYMENT_TARGET="$(RUST_MACOS_DEPLOYMENT_TARGET)" \ + CARGO_INCREMENTAL="$(CARGO_INCREMENTAL)" \ + RUSTC="$(RUSTC)" \ + RUSTFLAGS="$(RUSTFLAGS)" \ + TERM="dumb" \ + $(if $(RUST_OSX_SDK),SDKROOT="$(RUST_OSX_SDK)") \ + $(if $(RUSTUP_TOOLCHAIN),RUSTUP_TOOLCHAIN="$(RUSTUP_TOOLCHAIN)") + +# 'cargo' needs uppercase +RUST_NATIVE_ENV_UPPER = $(shell echo '$(RUST_NATIVE)' | tr 'a-z-' 'A-Z_') +RUST_NATIVE_LINKER = $(abs_top_builddir)/.cargo/native-linker +CARGO_ENV += \ + AR="$(NATIVE_AR)" \ + CC="$(NATIVE_CC)" \ + CXX="$(NATIVE_CXX)" \ + CARGO_TARGET_$(RUST_NATIVE_ENV_UPPER)_LINKER="$(RUST_NATIVE_LINKER)" + +# 'cc' needs preserved case +RUST_TARGET_ENV_LOWER = $(shell echo '$(RUST_TARGET)' | tr '-' '_') +# 'cargo' needs uppercase +RUST_TARGET_ENV_UPPER = $(shell echo '$(RUST_TARGET_ENV_LOWER)' | tr 'a-z' 'A-Z') +RUST_TARGET_LINKER = $(abs_top_builddir)/.cargo/target-linker +CARGO_ENV += \ + AR_$(RUST_TARGET_ENV_LOWER)="$(AR)" \ + CC_$(RUST_TARGET_ENV_LOWER)="$(CC)" \ + CFLAGS_$(RUST_TARGET_ENV_LOWER)="$(CFLAGS)" \ + CXX_$(RUST_TARGET_ENV_LOWER)="$(CXX)" \ + CXXFLAGS_$(RUST_TARGET_ENV_LOWER)="$(CXXFLAGS)" \ + CARGO_TARGET_$(RUST_TARGET_ENV_UPPER)_LINKER="$(RUST_TARGET_LINKER)" + +$(RUST_NATIVE_LINKER): Makefile + $(AM_V_at)$(MKDIR_P) $(abs_top_builddir)/.cargo + $(AM_V_at)printf '%s\n' '#!/bin/sh' 'exec $(NATIVE_CC) "$$@"' > $@ + $(AM_V_at)chmod +x $@ + +$(RUST_TARGET_LINKER): Makefile + $(AM_V_at)$(MKDIR_P) $(abs_top_builddir)/.cargo + $(AM_V_at)printf '%s\n' '#!/bin/sh' 'exec $(CC) "$$@"' > $@ + $(AM_V_at)chmod +x $@ + +if !ENABLE_DEBUG +CARGO_BUILD_OPTS += --release +endif # !ENABLE_DEBUG + +if !SILENT_RULES +CARGO_BUILD_OPTS += --verbose +endif # !SILENT_RULES + +if ENABLE_ONLINE_RUST +CARGO_CONFIGURED = $(abs_top_builddir)/.cargo/.configured-for-online + +$(CARGO_CONFIGURED): $(RUST_NATIVE_LINKER) $(RUST_TARGET_LINKER) + $(AM_V_at)rm -f $(abs_top_builddir)/.cargo/.configured-for-offline + $(AM_V_at)rm -f $(CARGO_BUILD_CONFIG) + $(AM_V_at)$(MKDIR_P) $(abs_top_builddir)/.cargo + $(AM_V_at)touch $@ +else +CARGO_BUILD_OPTS += --offline --config "$(CARGO_BUILD_CONFIG)" +CARGO_CONFIGURED = $(CARGO_BUILD_CONFIG) + +$(CARGO_CONFIGURED): $(RUST_NATIVE_LINKER) $(RUST_TARGET_LINKER) $(top_srcdir)/.cargo/config.toml.offline $(top_srcdir)/Cargo.lock + $(AM_V_at)rm -f $(abs_top_builddir)/.cargo/.configured-for-online + $(AM_V_at)$(MKDIR_P) $(abs_top_builddir)/.cargo + $(AM_V_at)cp $(top_srcdir)/.cargo/config.toml.offline $(CARGO_BUILD_CONFIG) + $(AM_V_at)echo "directory = \"$(RUST_VENDORED_SOURCES)\"" >> $(CARGO_BUILD_CONFIG) + $(AM_V_at)$(top_srcdir)/contrib/devtools/cargo-vendor-git-sources.sh $(top_srcdir)/Cargo.lock >> $(CARGO_BUILD_CONFIG) +endif # ENABLE_ONLINE_RUST diff --git a/rust/Makefile.libs.include b/rust/Makefile.libs.include new file mode 100644 index 000000000000..3bda542992af --- /dev/null +++ b/rust/Makefile.libs.include @@ -0,0 +1,62 @@ +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +if ENABLE_DEBUG +CARGO_PROFILE=debug +else +CARGO_PROFILE=release +endif + +# Per-crate includes contribute cxxbridge codegen artifacts (generated C++ +# sources/headers and dist file lists). Compilation happens through the +# umbrella crate `dashrust`, which cargo builds into one static library. +include $(top_srcdir)/rust/Makefile.chirp.include + +LIBRUSTDEPS_INCLUDES = \ + $(LIBRUST_CHIRP_INCLUDES) + +LIBRUSTDEPS_CPP = \ + $(LIBRUST_CHIRP_CPP) + +LIBRUSTDEPS_H = \ + $(LIBCXXBRIDGE_H) \ + $(LIBRUST_CHIRP_H) + +# Umbrella staticlib # +LIBRUSTDEPS_TARGET_DIR = $(abs_top_builddir)/rust/target +LIBRUSTDEPS_CRATE_DIR = $(top_srcdir)/rust/dashrust +LIBRUSTDEPS_MANIFEST = $(LIBRUSTDEPS_CRATE_DIR)/Cargo.toml +LIBRUSTDEPS = $(LIBRUSTDEPS_TARGET_DIR)/$(RUST_TARGET)/$(CARGO_PROFILE)/libdashrust.a + +LIBRUSTDEPS_CARGO_ENV = $(CARGO_ENV) CARGO_TARGET_DIR="$(LIBRUSTDEPS_TARGET_DIR)" +LIBRUSTDEPS_CARGO_BUILD_OPTS = $(CARGO_BUILD_OPTS) --manifest-path $(LIBRUSTDEPS_MANIFEST) + +LIBRUSTDEPS_SRCS = \ + $(LIBRUSTDEPS_CRATE_DIR)/src/lib.rs + +$(LIBRUSTDEPS_SRCS): ; + +LIBRUSTDEPS_FILES = \ + $(LIBRUSTDEPS_MANIFEST) \ + $(LIBRUSTDEPS_SRCS) + +LIBRUSTDEPS_COMPONENT_SRCS = \ + $(LIBRUST_CHIRP_FILES) + +$(LIBRUSTDEPS): $(CARGO_CONFIGURED) \ + $(top_srcdir)/Cargo.toml \ + $(top_srcdir)/Cargo.lock \ + $(LIBRUSTDEPS_SRCS) \ + $(LIBRUSTDEPS_MANIFEST) \ + $(LIBRUSTDEPS_COMPONENT_SRCS) + $(AM_V_GEN)$(LIBRUSTDEPS_CARGO_ENV) $(CARGO) build $(LIBRUSTDEPS_CARGO_BUILD_OPTS) + +cargo-build-dashrust: $(LIBRUSTDEPS) + +cargo-clean-dashrust: + $(AM_V_at)rm -rf $(LIBRUSTDEPS_TARGET_DIR) + +.PHONY: cargo-build-dashrust cargo-clean-dashrust +# diff --git a/rust/chirp/Cargo.toml b/rust/chirp/Cargo.toml new file mode 100644 index 000000000000..db79a79e40c8 --- /dev/null +++ b/rust/chirp/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "chirp" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +cxx = "1.0.198" + +[build-dependencies] +built = "0.8.0" diff --git a/rust/chirp/build.rs b/rust/chirp/build.rs new file mode 100644 index 000000000000..e57a97ad2171 --- /dev/null +++ b/rust/chirp/build.rs @@ -0,0 +1,3 @@ +fn main() { + built::write_built_file().expect("Failed to write build info"); +} diff --git a/rust/chirp/src/lib.rs b/rust/chirp/src/lib.rs new file mode 100644 index 000000000000..1d27c03613f3 --- /dev/null +++ b/rust/chirp/src/lib.rs @@ -0,0 +1,19 @@ +mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +#[cxx::bridge(namespace = "chirp")] +mod ffi { + extern "Rust" { + fn chirp() -> String; + } +} + +fn chirp() -> String { + format!( + "{} {} built with {} reports \"cheep cheep\"", + built_info::PKG_NAME, + built_info::PKG_VERSION, + built_info::RUSTC_VERSION, + ) +} diff --git a/rust/dashrust/Cargo.toml b/rust/dashrust/Cargo.toml new file mode 100644 index 000000000000..3f8ccfa6c7c4 --- /dev/null +++ b/rust/dashrust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dashrust" +version = "0.1.0" +edition = "2021" + +# Umbrella crate: the single staticlib linked into Dash Core binaries. +# Individual Rust components are rlib dependencies re-exported here, so cargo +# performs deduplication and LTO across the whole graph and emits one archive +# with no colliding object names. Optional components are cargo features so +# the build system can enable them per configure flags. + +[lib] +crate-type = ["staticlib"] + +[dependencies] +chirp = { path = "../chirp" } diff --git a/rust/dashrust/src/lib.rs b/rust/dashrust/src/lib.rs new file mode 100644 index 000000000000..b1107c19d269 --- /dev/null +++ b/rust/dashrust/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Umbrella crate bundling all Rust components linked into Dash Core. +//! +//! Each component keeps its own `#[cxx::bridge]`; the explicit `extern crate` +//! declarations root the component crates so their exported bridge symbols +//! survive into the staticlib. + +extern crate chirp; diff --git a/src/Makefile.am b/src/Makefile.am index fb1ffde2529a..9000284d507b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,5 +1,6 @@ # Copyright (c) 2013-2016 The Bitcoin Core developers -# Copyright (c) 2014-2018 The Dash Core developers +# Copyright (c) 2016-2025 The Zcash developers +# Copyright (c) 2014-2026 The Dash Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -7,6 +8,9 @@ print-%: FORCE @echo '$*'='$($*)' +include $(top_srcdir)/rust/Makefile.common.include +include $(top_srcdir)/rust/Makefile.libs.include + DIST_SUBDIRS = secp256k1 AM_LDFLAGS = $(LIBTOOL_LDFLAGS) $(HARDENED_LDFLAGS) $(SANITIZER_LDFLAGS) $(CORE_LDFLAGS) $(BACKTRACE_LDFLAGS) @@ -21,6 +25,13 @@ AM_CPPFLAGS = $(DEBUG_CPPFLAGS) $(HARDENED_CPPFLAGS) $(CORE_CPPFLAGS) AM_LIBTOOLFLAGS = --preserve-dup-deps PTHREAD_FLAGS = $(PTHREAD_CFLAGS) $(PTHREAD_LIBS) EXTRA_LIBRARIES = +BUILT_SOURCES = +if ENABLE_RUST +BUILT_SOURCES += $(LIBRUSTDEPS_H) + +libbitcoin_node_a-init.$(OBJEXT) \ +test/test_dash-system_tests.$(OBJEXT): $(LIBRUSTDEPS_H) +endif lib_LTLIBRARIES = noinst_LTLIBRARIES = @@ -48,6 +59,9 @@ endif # ENABLE_STACKTRACES BITCOIN_INCLUDES=-I$(builddir) -I$(srcdir)/$(MINISKETCH_INCLUDE_DIR_INT) -I$(srcdir)/secp256k1/include -I$(srcdir)/$(UNIVALUE_INCLUDE_DIR_INT) $(LEVELDB_CPPFLAGS) BITCOIN_INCLUDES+=-isystem$(srcdir)/dashbls/include -isystem$(srcdir)/dashbls/depends/relic/include -isystem$(srcdir)/dashbls/depends/minialloc/include BITCOIN_INCLUDES+=-isystem$(srcdir)/immer +if ENABLE_RUST +BITCOIN_INCLUDES+=$(LIBRUSTDEPS_INCLUDES) +endif LIBBITCOIN_NODE=libbitcoin_node.a LIBBITCOIN_COMMON=libbitcoin_common.a @@ -62,6 +76,9 @@ LIBSECP256K1=secp256k1/libsecp256k1.la if ENABLE_ZMQ LIBBITCOIN_ZMQ=libbitcoin_zmq.a endif +if ENABLE_PLATFORM_GUI +LIBDASH_PLATFORM=libdash_platform.a +endif if BUILD_BITCOIN_LIBS LIBBITCOINCONSENSUS=libdashconsensus.la endif @@ -116,6 +133,14 @@ $(LIBDASHBLS): $(LIBSECP256K1): $(wildcard secp256k1/src/*.h) $(wildcard secp256k1/src/*.c) $(wildcard secp256k1/include/*) $(AM_V_at)$(MAKE) $(AM_MAKEFLAGS) -C $(@D) $(@F) +if ENABLE_RUST +LIBCXXBRIDGE=libcxxbridge.la +# libcxxbridge is the generated C++ side of the in-tree chirp bridge, and +# libdashrust is its Rust implementation. +RUST_COMPONENT_LIBS=$(LIBCXXBRIDGE) $(LIBRUSTDEPS) $(LIBCXXBRIDGE) $(LIBRUSTDEPS) +RUST_SYSTEM_LIBS=$(RUST_LIBS) +endif + # Make is not made aware of per-object dependencies to avoid limiting building parallelization # But to build the less dependent modules first, we manually select their order here: EXTRA_LIBRARIES += \ @@ -127,7 +152,12 @@ EXTRA_LIBRARIES += \ $(LIBBITCOIN_IPC) \ $(LIBBITCOIN_WALLET) \ $(LIBBITCOIN_WALLET_TOOL) \ - $(LIBBITCOIN_ZMQ) + $(LIBBITCOIN_ZMQ) \ + $(LIBDASH_PLATFORM) + +if ENABLE_RUST +noinst_LTLIBRARIES += $(LIBCXXBRIDGE) +endif if BUILD_BITCOIND bin_PROGRAMS += dashd @@ -232,6 +262,7 @@ BITCOIN_CORE_H = \ evo/mnhftx.h \ evo/netinfo.h \ evo/providertx.h \ + evo/providertx_service.h \ evo/simplifiedmns.h \ evo/smldiff.h \ evo/specialtx.h \ @@ -281,6 +312,7 @@ BITCOIN_CORE_H = \ interfaces/init.h \ interfaces/ipc.h \ interfaces/node.h \ + interfaces/providertx.h \ interfaces/wallet.h \ kernel/blockmanager_opts.h \ kernel/chain.h \ @@ -370,7 +402,7 @@ BITCOIN_CORE_H = \ rest.h \ rpc/blockchain.h \ rpc/client.h \ - rpc/evo_util.h \ + rpc/json_help.h \ rpc/mempool.h \ rpc/mining.h \ rpc/protocol.h \ @@ -476,6 +508,8 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/ismine.h \ wallet/load.h \ + wallet/platformkeys.h \ + wallet/platformtypes.h \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ @@ -539,6 +573,7 @@ libbitcoin_node_a_SOURCES = \ evo/mnauth.cpp \ evo/mnhftx.cpp \ evo/providertx.cpp \ + evo/providertx_service.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/specialtx.cpp \ @@ -635,6 +670,7 @@ libbitcoin_node_a_SOURCES = \ rpc/evo.cpp \ rpc/fees.cpp \ rpc/governance.cpp \ + rpc/json_help.cpp \ rpc/masternode.cpp \ rpc/mempool.cpp \ rpc/mining.cpp \ @@ -685,6 +721,40 @@ libbitcoin_zmq_a_SOURCES = \ endif # +# platform (Dash Platform client, linked into dash-qt and test_dash only) # +if ENABLE_PLATFORM_GUI +libdash_platform_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(BOOST_CPPFLAGS) $(MBEDTLS_CFLAGS) $(PLATFORM_CXX_CFLAGS) +libdash_platform_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) +libdash_platform_a_CFLAGS = $(AM_CFLAGS) $(PIE_FLAGS) +libdash_platform_a_SOURCES = \ + platform/client.h \ + platform/dpp/document.cpp \ + platform/dpp/document.h \ + platform/dpp/identity.cpp \ + platform/dpp/identity.h \ + platform/dpp/statetransitions.cpp \ + platform/drive/queries.cpp \ + platform/drive/queries.h \ + platform/params.cpp \ + platform/params.h \ + platform/statetransitions.h \ + platform/transport/cbor.h \ + platform/transport/client.cpp \ + platform/transport/endpoint_retry.h \ + platform/transport/freshness.h \ + platform/transport/grpcweb.cpp \ + platform/transport/grpcweb.h \ + platform/transport/protobuf.cpp \ + platform/transport/protobuf.h \ + platform/transport/tls.cpp \ + platform/transport/tls.h \ + platform/types.h \ + platform/walletrecords.cpp \ + platform/walletrecords.h + +endif +# + # wallet # libbitcoin_wallet_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(BOOST_CPPFLAGS) $(BDB_CPPFLAGS) $(SQLITE_CFLAGS) libbitcoin_wallet_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) @@ -706,6 +776,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/hdchain.cpp \ wallet/interfaces.cpp \ wallet/load.cpp \ + wallet/platformkeys.cpp \ wallet/receive.cpp \ wallet/rpc/addresses.cpp \ wallet/rpc/backup.cpp \ @@ -966,7 +1037,6 @@ libbitcoin_common_a_SOURCES = \ evo/providertx_util.cpp \ external_signer.cpp \ governance/common.cpp \ - governance/core_write.cpp \ init/common.cpp \ key.cpp \ key_io.cpp \ @@ -981,7 +1051,6 @@ libbitcoin_common_a_SOURCES = \ policy/policy.cpp \ protocol.cpp \ psbt.cpp \ - rpc/evo_util.cpp \ rpc/external_signer.cpp \ rpc/rawtransaction_util.cpp \ rpc/request.cpp \ @@ -1091,9 +1160,10 @@ bitcoin_bin_ldadd = \ $(LIBDASHBLS) \ $(LIBLEVELDB) \ $(LIBMEMENV) \ - $(LIBSECP256K1) + $(LIBSECP256K1) \ + $(RUST_COMPONENT_LIBS) -bitcoin_bin_ldadd += $(BACKTRACE_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(ZMQ_LIBS) $(GMP_LIBS) +bitcoin_bin_ldadd += $(BACKTRACE_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(ZMQ_LIBS) $(GMP_LIBS) $(RUST_SYSTEM_LIBS) dashd_SOURCES = $(bitcoin_daemon_sources) init/bitcoind.cpp dashd_CPPFLAGS = $(bitcoin_bin_cppflags) @@ -1407,6 +1477,17 @@ libdashconsensus_la_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) endif # +# CXX bridge library # +if ENABLE_RUST +libcxxbridge_la_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(PIC_FLAGS) +libcxxbridge_la_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) $(PIC_FLAGS) -static +libcxxbridge_la_LDFLAGS = $(AM_LDFLAGS) -static +nodist_libcxxbridge_la_SOURCES = $(LIBRUSTDEPS_CPP) + +$(libcxxbridge_la_OBJECTS): $(LIBRUSTDEPS_H) +endif +# + CTAES_DIST = crypto/ctaes/bench.c CTAES_DIST += crypto/ctaes/ctaes.c CTAES_DIST += crypto/ctaes/ctaes.h diff --git a/src/Makefile.bench.include b/src/Makefile.bench.include index 7dec51f2b0c1..78a51e5f5128 100644 --- a/src/Makefile.bench.include +++ b/src/Makefile.bench.include @@ -77,12 +77,14 @@ bench_bench_dash_LDADD = \ $(LIBLEVELDB) \ $(LIBMEMENV) \ $(LIBSECP256K1) \ + $(RUST_COMPONENT_LIBS) \ $(LIBUNIVALUE) \ $(EVENT_PTHREADS_LIBS) \ $(EVENT_LIBS) \ $(MINIUPNPC_LIBS) \ $(NATPMP_LIBS) \ $(GMP_LIBS) \ + $(RUST_SYSTEM_LIBS) \ $(BACKTRACE_LIBS) if ENABLE_ZMQ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 16296207eca1..8d7916b82bde 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -114,6 +114,21 @@ QT_MOC_CPP = \ qt/moc_walletmodel.cpp \ qt/moc_walletview.cpp +if ENABLE_PLATFORM_GUI +QT_MOC_CPP += \ + qt/platform/moc_contactflow.cpp \ + qt/platform/moc_contactpickerdialog.cpp \ + qt/platform/moc_contactsmodel.cpp \ + qt/platform/moc_contactspage.cpp \ + qt/platform/moc_createusernamewizard.cpp \ + qt/platform/moc_identityflow.cpp \ + qt/platform/moc_platformpage.cpp \ + qt/platform/moc_platformrecovery.cpp \ + qt/platform/moc_platformservice.cpp \ + qt/platform/moc_profiledialog.cpp \ + qt/platform/moc_usernamesearchdialog.cpp +endif + BITCOIN_MM = \ qt/macdockiconhandler.mm \ qt/macnotificationhandler.mm \ @@ -207,6 +222,21 @@ BITCOIN_QT_H = \ qt/walletview.h \ qt/winshutdownmonitor.h +if ENABLE_PLATFORM_GUI +BITCOIN_QT_H += \ + qt/platform/contactflow.h \ + qt/platform/contactpickerdialog.h \ + qt/platform/contactsmodel.h \ + qt/platform/contactspage.h \ + qt/platform/createusernamewizard.h \ + qt/platform/identityflow.h \ + qt/platform/platformpage.h \ + qt/platform/platformrecovery.h \ + qt/platform/platformservice.h \ + qt/platform/profiledialog.h \ + qt/platform/usernamesearchdialog.h +endif + QT_RES_ICONS = \ qt/res/icons/address-book.png \ qt/res/icons/connect1_16.png \ @@ -331,12 +361,28 @@ BITCOIN_QT_WALLET_CPP = \ qt/walletmodeltransaction.cpp \ qt/walletview.cpp +BITCOIN_QT_PLATFORM_CPP = \ + qt/platform/contactflow.cpp \ + qt/platform/contactpickerdialog.cpp \ + qt/platform/contactsmodel.cpp \ + qt/platform/contactspage.cpp \ + qt/platform/createusernamewizard.cpp \ + qt/platform/identityflow.cpp \ + qt/platform/platformpage.cpp \ + qt/platform/platformrecovery.cpp \ + qt/platform/platformservice.cpp \ + qt/platform/profiledialog.cpp \ + qt/platform/usernamesearchdialog.cpp + BITCOIN_QT_CPP = $(BITCOIN_QT_BASE_CPP) if TARGET_WINDOWS BITCOIN_QT_CPP += $(BITCOIN_QT_WINDOWS_CPP) endif if ENABLE_WALLET BITCOIN_QT_CPP += $(BITCOIN_QT_WALLET_CPP) +if ENABLE_PLATFORM_GUI +BITCOIN_QT_CPP += $(BITCOIN_QT_PLATFORM_CPP) +endif # ENABLE_PLATFORM_GUI endif # ENABLE_WALLET QT_RES_IMAGES = \ @@ -461,9 +507,12 @@ endif if ENABLE_ZMQ bitcoin_qt_ldadd += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif +if ENABLE_PLATFORM_GUI +bitcoin_qt_ldadd += $(LIBDASH_PLATFORM) $(PLATFORM_CXX_LIBS) $(MBEDTLS_LIBS) +endif bitcoin_qt_ldadd += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) \ - $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ - $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) + $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) $(RUST_COMPONENT_LIBS) \ + $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) $(RUST_SYSTEM_LIBS) bitcoin_qt_ldflags = $(LDFLAGS_WRAP_EXCEPTIONS) $(RELDFLAGS) $(AM_LDFLAGS) $(QT_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) $(PTHREAD_FLAGS) bitcoin_qt_libtoolflags = $(AM_LIBTOOLFLAGS) --tag CXX @@ -492,7 +541,7 @@ $(srcdir)/qt/dashstrings.cpp: FORCE # The resulted dash_en.xlf source file should follow Transifex requirements. # See: https://docs.transifex.com/formats/xliff#how-to-distinguish-between-a-source-file-and-a-translation-file -translate: $(srcdir)/qt/dashstrings.cpp $(QT_FORMS_UI) $(QT_FORMS_UI) $(BITCOIN_QT_BASE_CPP) qt/bitcoin.cpp $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) +translate: $(srcdir)/qt/dashstrings.cpp $(QT_FORMS_UI) $(QT_FORMS_UI) $(BITCOIN_QT_BASE_CPP) qt/bitcoin.cpp $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_PLATFORM_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) @test -n $(LUPDATE) || echo "lupdate is required for updating translations" $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(LUPDATE) -no-obsolete -I $(srcdir) -locations relative $^ -ts $(srcdir)/qt/locale/dash_en.ts @test -n $(LCONVERT) || echo "lconvert is required for updating translations" @@ -528,6 +577,7 @@ ui_%.h: %.ui $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(MOC) $(DEFAULT_INCLUDES) $(QT_INCLUDES_UNSUPPRESSED) $(MOC_DEFS) $< > $@ moc_%.cpp: %.h + @$(MKDIR_P) $(@D) $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(MOC) $(DEFAULT_INCLUDES) $(QT_INCLUDES_UNSUPPRESSED) $(MOC_DEFS) $< > $@ %.qm: %.ts diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index 62071d40e5a5..9ed92298193d 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -16,6 +16,7 @@ TEST_QT_MOC_CPP = \ if ENABLE_WALLET TEST_QT_MOC_CPP += \ qt/test/moc_addressbooktests.cpp \ + qt/test/moc_providertransactiontests.cpp \ qt/test/moc_wallettests.cpp endif # ENABLE_WALLET @@ -23,6 +24,7 @@ TEST_QT_H = \ qt/test/addressbooktests.h \ qt/test/apptests.h \ qt/test/optiontests.h \ + qt/test/providertransactiontests.h \ qt/test/rpcnestedtests.h \ qt/test/uritests.h \ qt/test/util.h \ @@ -45,6 +47,7 @@ qt_test_test_dash_qt_SOURCES = \ if ENABLE_WALLET qt_test_test_dash_qt_SOURCES += \ qt/test/addressbooktests.cpp \ + qt/test/providertransactiontests.cpp \ qt/test/wallettests.cpp \ wallet/test/wallet_test_fixture.cpp endif # ENABLE_WALLET @@ -58,10 +61,13 @@ endif if ENABLE_ZMQ qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif +if ENABLE_PLATFORM_GUI +qt_test_test_dash_qt_LDADD += $(LIBDASH_PLATFORM) $(PLATFORM_CXX_LIBS) $(MBEDTLS_LIBS) +endif qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) \ $(LIBMEMENV) $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QT_TEST_LIBS) \ - $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ - $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) + $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) $(RUST_COMPONENT_LIBS) \ + $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) $(RUST_SYSTEM_LIBS) qt_test_test_dash_qt_LDFLAGS = $(LDFLAGS_WRAP_EXCEPTIONS) $(RELDFLAGS) $(AM_LDFLAGS) $(QT_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) $(PTHREAD_FLAGS) qt_test_test_dash_qt_CXXFLAGS = $(AM_CXXFLAGS) $(QT_PIE_FLAGS) diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 427909b8e12f..33527c0bbc85 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -56,12 +56,18 @@ FUZZ_SUITE_LD_COMMON = \ $(LIBLEVELDB) \ $(LIBMEMENV) \ $(LIBSECP256K1) \ + $(RUST_COMPONENT_LIBS) \ $(MINISKETCH_LIBS) \ $(EVENT_LIBS) \ $(EVENT_PTHREADS_LIBS) \ $(GMP_LIBS) \ + $(RUST_SYSTEM_LIBS) \ $(BACKTRACE_LIBS) +if ENABLE_PLATFORM_GUI +FUZZ_SUITE_LD_COMMON += $(LIBDASH_PLATFORM) $(PLATFORM_CXX_LIBS) $(MBEDTLS_LIBS) +endif + if USE_UPNP FUZZ_SUITE_LD_COMMON += $(MINIUPNPC_LIBS) endif @@ -220,10 +226,23 @@ BITCOIN_TESTS =\ test/versionbits_tests.cpp \ test/xoroshiro128plusplus_tests.cpp +if ENABLE_PLATFORM_GUI +BITCOIN_TESTS += \ + test/platform_client_tests.cpp \ + test/platform_dpp_tests.cpp \ + test/platform_drive_tests.cpp +JSON_TEST_FILES += \ + test/data/platform/dpp_identity_vectors.json \ + test/data/platform/dpp_st_vectors.json \ + test/data/platform/drive_query_vectors.json \ + test/data/platform/quorum_sig_vectors.json +endif + if ENABLE_WALLET BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ wallet/test/coinjoin_tests.cpp \ + wallet/test/platformkeys_tests.cpp \ wallet/test/psbt_wallet_tests.cpp \ wallet/test/spend_tests.cpp \ wallet/test/wallet_tests.cpp \ @@ -235,7 +254,8 @@ BITCOIN_TESTS += \ wallet/test/init_tests.cpp \ wallet/test/ismine_tests.cpp \ wallet/test/rpc_util_tests.cpp \ - wallet/test/scriptpubkeyman_tests.cpp + wallet/test/scriptpubkeyman_tests.cpp \ + wallet/test/walletload_tests.cpp FUZZ_SUITE_LD_COMMON +=\ $(SQLITE_LIBS) \ @@ -271,11 +291,14 @@ if ENABLE_WALLET test_test_dash_LDADD += $(LIBBITCOIN_WALLET) test_test_dash_CPPFLAGS += $(BDB_CPPFLAGS) endif +if ENABLE_PLATFORM_GUI +test_test_dash_LDADD += $(LIBDASH_PLATFORM) $(PLATFORM_CXX_LIBS) $(MBEDTLS_LIBS) +endif test_test_dash_LDADD += $(LIBBITCOIN_NODE) $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBUNIVALUE) \ - $(LIBDASHBLS) $(LIBLEVELDB) $(LIBMEMENV) $(BACKTRACE_LIBS) $(LIBSECP256K1) $(EVENT_LIBS) $(EVENT_PTHREADS_LIBS) $(MINISKETCH_LIBS) + $(LIBDASHBLS) $(LIBLEVELDB) $(LIBMEMENV) $(BACKTRACE_LIBS) $(LIBSECP256K1) $(RUST_COMPONENT_LIBS) $(EVENT_LIBS) $(EVENT_PTHREADS_LIBS) $(MINISKETCH_LIBS) test_test_dash_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) -test_test_dash_LDADD += $(BDB_LIBS) $(MINIUPNPC_LIBS) $(SQLITE_LIBS) $(NATPMP_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) +test_test_dash_LDADD += $(BDB_LIBS) $(MINIUPNPC_LIBS) $(SQLITE_LIBS) $(NATPMP_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) $(RUST_SYSTEM_LIBS) test_test_dash_LDFLAGS = $(LDFLAGS_WRAP_EXCEPTIONS) $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) $(PTHREAD_FLAGS) -static if ENABLE_ZMQ @@ -321,6 +344,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/decode_tx.cpp \ test/fuzz/descriptor_parse.cpp \ test/fuzz/deserialize.cpp \ + test/fuzz/dkg_message_framing.cpp \ test/fuzz/eval_script.cpp \ test/fuzz/fee_rate.cpp \ test/fuzz/fees.cpp \ @@ -396,6 +420,10 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/utxo_total_supply.cpp \ test/fuzz/validation_load_mempool.cpp \ test/fuzz/versionbits.cpp + +if ENABLE_PLATFORM_GUI +test_fuzz_fuzz_SOURCES += test/fuzz/platform_bridge.cpp +endif endif # ENABLE_FUZZ_BINARY nodist_test_test_dash_SOURCES = $(GENERATED_TEST_FILES) diff --git a/src/active/dkgsessionhandler.cpp b/src/active/dkgsessionhandler.cpp index 56cfedf0d42f..a6464a89e712 100644 --- a/src/active/dkgsessionhandler.cpp +++ b/src/active/dkgsessionhandler.cpp @@ -193,7 +193,7 @@ void ActiveDKGSessionHandler::SleepBeforePhase(QuorumPhase curPhase, const uint2 // Don't expect perfect block times and thus reduce the phase time to be on the secure side (caller chooses factor) double adjustedPhaseSleepTimePerMember = phaseSleepTimePerMember * randomSleepFactor; - int64_t sleepTime = (int64_t)(adjustedPhaseSleepTimePerMember * curSession->GetMyMemberIndex().value_or(0)); + int64_t sleepTime = static_cast(adjustedPhaseSleepTimePerMember * curSession->GetMyMemberIndex().value_or(0)); const auto endTime = SteadyClock::now() + std::chrono::milliseconds{sleepTime}; int heightTmp{currentHeight.load()}; int heightStart{heightTmp}; diff --git a/src/bench/load_external.cpp b/src/bench/load_external.cpp index ec97be45ff1e..e11766929430 100644 --- a/src/bench/load_external.cpp +++ b/src/bench/load_external.cpp @@ -48,14 +48,13 @@ static void LoadExternalBlockFile(benchmark::Bench& bench) fclose(file); } - Chainstate& chainstate{testing_setup->m_node.chainman->ActiveChainstate()}; std::multimap blocks_with_unknown_parent; FlatFilePos pos; bench.run([&] { // "rb" is "binary, O_RDONLY", positioned to the start of the file. // The file will be closed by LoadExternalBlockFile(). FILE* file{fsbridge::fopen(blkfile, "rb")}; - chainstate.LoadExternalBlockFile(file, &pos, &blocks_with_unknown_parent); + testing_setup->m_node.chainman->LoadExternalBlockFile(file, &pos, &blocks_with_unknown_parent); }); fs::remove(blkfile); } diff --git a/src/bls/bls.cpp b/src/bls/bls.cpp index 49981ee9190f..8d5b1b83703c 100644 --- a/src/bls/bls.cpp +++ b/src/bls/bls.cpp @@ -8,6 +8,7 @@ #ifndef BUILD_BITCOIN_INTERNAL #include +#include #endif #include @@ -73,6 +74,7 @@ void CBLSSecretKey::MakeNewKey() } catch (...) { } } + memory_cleanse(buf, sizeof(buf)); fValid = true; cachedHash.SetNull(); } diff --git a/src/bls/bls_ies.cpp b/src/bls/bls_ies.cpp index e54bb36e1c35..84d2a467e300 100644 --- a/src/bls/bls_ies.cpp +++ b/src/bls/bls_ies.cpp @@ -14,8 +14,8 @@ static bool EncryptBlob(const void* in, size_t inSize, std::vector(symKey), reinterpret_cast(iv), false); - int w = enc.Encrypt(reinterpret_cast(in), int(inSize), reinterpret_cast(out.data())); - return w == int(inSize); + int w = enc.Encrypt(reinterpret_cast(in), static_cast(inSize), reinterpret_cast(out.data())); + return w == static_cast(inSize); } template @@ -24,8 +24,8 @@ static bool DecryptBlob(const void* in, size_t inSize, Out& out, const void* sym out.resize(inSize); AES256CBCDecrypt enc(reinterpret_cast(symKey), reinterpret_cast(iv), false); - int w = enc.Decrypt(reinterpret_cast(in), int(inSize), reinterpret_cast(out.data())); - return w == (int)inSize; + int w = enc.Decrypt(reinterpret_cast(in), static_cast(inSize), reinterpret_cast(out.data())); + return w == static_cast(inSize); } uint256 CBLSIESEncryptedBlob::GetIV(size_t idx) const diff --git a/src/bls/bls_worker.cpp b/src/bls/bls_worker.cpp index 2505bdcab827..eade0642bc44 100644 --- a/src/bls/bls_worker.cpp +++ b/src/bls/bls_worker.cpp @@ -72,8 +72,8 @@ void CBLSWorker::Stop() #ifndef BUILD_BITCOIN_INTERNAL bool CBLSWorker::GenerateContributions(int quorumThreshold, Span ids, BLSVerificationVectorPtr& vvecRet, std::vector& skSharesRet) { - auto svec = std::vector((size_t)quorumThreshold); - vvecRet = std::make_shared>((size_t)quorumThreshold); + auto svec = std::vector(static_cast(quorumThreshold)); + vvecRet = std::make_shared>(static_cast(quorumThreshold)); skSharesRet.resize(ids.size()); for (int i = 0; i < quorumThreshold; i++) { @@ -83,7 +83,7 @@ bool CBLSWorker::GenerateContributions(int quorumThreshold, Span ids, BL std::vector> futures; futures.reserve((quorumThreshold / batchSize + ids.size() / batchSize) + 2); - for (size_t i = 0; i < size_t(quorumThreshold); i += batchSize) { + for (size_t i = 0; i < static_cast(quorumThreshold); i += batchSize) { size_t start = i; size_t count = std::min(batchSize, quorumThreshold - start); auto f = [&, start, count](int threadId) { diff --git a/src/chain.h b/src/chain.h index b4022f2a95d4..f7f1d24fb8a4 100644 --- a/src/chain.h +++ b/src/chain.h @@ -98,10 +98,10 @@ enum BlockStatus : uint32_t { BLOCK_VALID_TRANSACTIONS = 3, //! Outputs do not overspend inputs, no double spends, coinbase output ok, no immature coinbase spends, BIP30. - //! Implies all parents are also at least CHAIN. + //! Implies all parents are either at least VALID_CHAIN, or are ASSUMED_VALID BLOCK_VALID_CHAIN = 4, - //! Scripts & signatures ok. Implies all parents are also at least SCRIPTS. + //! Scripts & signatures ok. Implies all parents are either at least VALID_SCRIPTS, or are ASSUMED_VALID. BLOCK_VALID_SCRIPTS = 5, //! All validity bits. @@ -119,10 +119,18 @@ enum BlockStatus : uint32_t { BLOCK_CONFLICT_CHAINLOCK = 128, //!< conflicts with chainlock system /** - * If set, this indicates that the block index entry is assumed-valid. - * Certain diagnostics will be skipped in e.g. CheckBlockIndex(). - * It almost certainly means that the block's full validation is pending - * on a background chainstate. See `doc/design/assumeutxo.md`. + * If ASSUMED_VALID is set, it means that this block has not been validated + * and has validity status less than VALID_SCRIPTS. Also that it may have + * descendant blocks with VALID_SCRIPTS set, because they can be validated + * based on an assumeutxo snapshot. + * + * When an assumeutxo snapshot is loaded, the ASSUMED_VALID flag is added to + * unvalidated blocks at the snapshot height and below. Then, as the background + * validation progresses, and these blocks are validated, the ASSUMED_VALID + * flags are removed. See `doc/design/assumeutxo.md` for details. + * + * This flag is only used to implement checks in CheckBlockIndex() and + * should not be used elsewhere. */ BLOCK_ASSUMED_VALID = 256, }; diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 1a16f48acadd..7f777e2174eb 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -25,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -181,6 +181,12 @@ void CCoinJoinClientSession::SetNull() mixingMasternode = nullptr; pendingDsaRequest = CPendingDsaRequest(); + // Post-V24: rebalance inputs are recorded in vecOutPointLocked at lock time + // (SelectRebalanceInputs), so UnlockCoins() releases them - just clear the bookkeeping + m_fPromotion = false; + m_fDemotion = false; + m_vecRebalanceInputs.clear(); + CCoinJoinBaseSession::SetNull(); } @@ -378,13 +384,21 @@ bool CCoinJoinClientSession::SendDenominate(const std::vector vecTxOutTmp; for (const auto& [txDsIn, txOut] : vecPSInOutPairsIn) { - vecTxDSInTmp.emplace_back(txDsIn); - vecTxOutTmp.emplace_back(txOut); - tx.vin.emplace_back(txDsIn); - tx.vout.emplace_back(txOut); + // For promotion/demotion, filter out empty inputs/outputs + // Promotion: 10 inputs with only 1 real output (others are empty) + // Demotion: 1 input with 10 outputs (only first has real input) + if (!txDsIn.prevout.IsNull()) { + vecTxDSInTmp.emplace_back(txDsIn); + tx.vin.emplace_back(txDsIn); + } + if (txOut.nValue > 0) { + vecTxOutTmp.emplace_back(txOut); + tx.vout.emplace_back(txOut); + } } - WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::SendDenominate -- Submitting partial tx %s", tx.ToString()); /* Continued */ + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::SendDenominate -- Submitting partial tx with %d inputs, %d outputs: %s\n", + vecTxDSInTmp.size(), vecTxOutTmp.size(), tx.ToString()); // store our entry for later use LOCK(cs_coinjoin); @@ -452,6 +466,13 @@ bool CCoinJoinClientSession::SignFinalTransaction(CNode& peer, Chainstate& activ if (!mixingMasternode) return false; + // Evaluated before taking cs_wallet (IsPromotionDemotionActive locks cs_main). + // A session we joined for 1:1 mixing can still admit a rebalance participant, so the final + // tx may legitimately be unbalanced while our own tip is one block short of V24 - refusing + // to sign it would cost us our collateral. Further behind than that is our own problem. + const bool fRebalanceShapesPossible = + CoinJoin::IsPromotionDemotionActive(active_chainstate.m_chainman, /*fNextBlock=*/true); + LOCK(m_wallet->cs_wallet); LOCK(cs_coinjoin); @@ -474,8 +495,9 @@ bool CCoinJoinClientSession::SignFinalTransaction(CNode& peer, Chainstate& activ // Make sure all inputs/outputs are valid PoolMessage nMessageID{MSG_NOERR}; + CoinJoin::SessionDenomCounts denomCounts; if (!IsValidInOuts(active_chainstate, m_isman, mempool, finalMutableTransaction.vin, finalMutableTransaction.vout, - nMessageID, nullptr)) { + nSessionDenom, /*fAllowRebalanceShapes=*/fRebalanceShapesPossible, nMessageID, nullptr, /*fFinalTx=*/true, &denomCounts)) { WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- ERROR! IsValidInOuts() failed: %s\n", __func__, CoinJoin::GetMessageByID(nMessageID).translated); UnlockCoins(); keyHolderStorage.ReturnAll(); @@ -533,6 +555,38 @@ bool CCoinJoinClientSession::SignFinalTransaction(CNode& peer, Chainstate& activ } } + // Post-V24: refuse to sign unless somebody else holds coins at the session denomination on + // whichever side we do. Only same-denomination coins conceal ours - coins at the larger + // adjacent denomination are told apart by amount - so a malicious masternode could + // otherwise pad the transaction with larger-denom coins and publish one that links our ten + // promotion inputs, or our ten demotion outputs, to each other in plain sight. The server + // never finalizes a session where a side has a lone occupant, so this never rejects an + // honest final transaction. + if (fRebalanceShapesPossible) { + const CAmount nSessionAmount = CoinJoin::DenominationToAmount(nSessionDenom); + size_t nOwnSessionInputs{0}; + size_t nOwnSessionOutputs{0}; + for (const auto& entry : vecEntries) { + // Our own promotion inputs and standard inputs are at the session denomination; a + // demotion spends a single larger-denom coin, which needs no cover of its own. + if (entry.GetMixShape() != CoinJoin::MixShape::DEMOTION) nOwnSessionInputs += entry.vecTxDSIn.size(); + for (const auto& txout : entry.vecTxOut) { + if (txout.nValue == nSessionAmount) ++nOwnSessionOutputs; + } + } + + const bool fInputsCovered = nOwnSessionInputs == 0 || denomCounts.inputs > nOwnSessionInputs; + const bool fOutputsCovered = nOwnSessionOutputs == 0 || denomCounts.outputs > nOwnSessionOutputs; + if (!fInputsCovered || !fOutputsCovered) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- no cover at the session denom (inputs %d/%d, outputs %d/%d), refusing to sign!\n", + __func__, nOwnSessionInputs, denomCounts.inputs, nOwnSessionOutputs, denomCounts.outputs); + UnlockCoins(); + keyHolderStorage.ReturnAll(); + SetNull(); + return false; + } + } + // fill values for found outpoints m_wallet->chain().findCoins(coins); std::map signing_errors; @@ -916,7 +970,13 @@ bool CCoinJoinClientSession::DoAutomaticDenominating(ChainstateManager& chainman if (!CCoinJoinClientOptions::IsEnabled()) return false; + // Post-V24: whether denomination promotion/demotion is active. Evaluated before taking + // cs_wallet (IsPromotionDemotionActive locks cs_main). + const bool fV24Active = CoinJoin::IsPromotionDemotionActive(chainman); + CAmount nBalanceNeedsAnonymized; + wallet::CoinJoinDenomCounts denomCounts; + bool fRebalanceOpportunity{false}; { LOCK(m_wallet->cs_wallet); @@ -950,7 +1010,22 @@ bool CCoinJoinClientSession::DoAutomaticDenominating(ChainstateManager& chainman CAmount nBalanceAnonymized = bal.m_anonymized; nBalanceNeedsAnonymized = CCoinJoinClientOptions::GetAmount() * COIN - nBalanceAnonymized; - if (nBalanceNeedsAnonymized < 0) { + // Post-V24: check for promotion/demotion opportunities before the "nothing to do" + // exits below - rebalancing matters precisely when the anonymization target is + // reached and all coins are already fully mixed + if (fV24Active) { + // Single wallet scan covering all denominations, reused across every + // adjacent-pair check instead of re-scanning per pair. + denomCounts = m_wallet->GetDenominationCounts(); + for (size_t i = 0; i + 1 < CoinJoin::vecStandardDenominations.size() && !fRebalanceOpportunity; ++i) { + const int nLargerDenom = 1 << i; + const int nSmallerDenom = 1 << (i + 1); + fRebalanceOpportunity = m_clientman.ShouldPromote(nSmallerDenom, nLargerDenom, denomCounts) || + m_clientman.ShouldDemote(nLargerDenom, nSmallerDenom, denomCounts); + } + } + + if (nBalanceNeedsAnonymized < 0 && !fRebalanceOpportunity) { WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::DoAutomaticDenominating -- Nothing to do\n"); // nothing to do, just keep it in idle mode return false; @@ -967,8 +1042,9 @@ bool CCoinJoinClientSession::DoAutomaticDenominating(ChainstateManager& chainman // including denoms but applying some restrictions CAmount nBalanceAnonymizable = m_wallet->GetAnonymizableBalance(); - // mixable balance is way too small - if (nBalanceAnonymizable < nValueMin) { + // mixable balance is way too small; note that fully-mixed coins don't count as + // anonymizable, so a rebalance opportunity must bypass this check too + if (nBalanceAnonymizable < nValueMin && !fRebalanceOpportunity) { strAutoDenomResult = _("Not enough funds to mix."); WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::DoAutomaticDenominating -- %s\n", strAutoDenomResult.original); return false; @@ -1072,13 +1148,63 @@ bool CCoinJoinClientSession::DoAutomaticDenominating(ChainstateManager& chainman } } // LOCK(m_wallet->cs_wallet); - // Always attempt to join an existing queue - if (JoinExistingQueue(nBalanceNeedsAnonymized, connman)) { - return true; + // Post-V24: Check if we should promote or demote denominations + // This helps maintain optimal denomination distribution as coins are spent + if (fRebalanceOpportunity) { + // Check all adjacent denomination pairs for promotion/demotion opportunities + // Denominations: 10, 1, 0.1, 0.01, 0.001 (indices 0-4, smaller index = larger denom) + for (size_t i = 0; i + 1 < CoinJoin::vecStandardDenominations.size(); ++i) { + const int nLargerDenom = 1 << i; // Larger denomination (e.g., 10 DASH) + const int nSmallerDenom = 1 << (i + 1); // Smaller denomination (e.g., 1 DASH) + + // Check if we should promote smaller -> larger; ShouldPromote() requires + // PROMOTION_RATIO fully-mixed coins, the queue functions re-verify on selection + if (m_clientman.ShouldPromote(nSmallerDenom, nLargerDenom, denomCounts)) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::DoAutomaticDenominating -- Promotion opportunity: %d x %s -> 1 x %s\n", + CoinJoin::PROMOTION_RATIO, + CoinJoin::DenominationToString(nSmallerDenom), + CoinJoin::DenominationToString(nLargerDenom)); + + // Try to join an existing queue for promotion + if (JoinExistingQueue(nBalanceNeedsAnonymized, connman, nSmallerDenom, /*fPromotion=*/true)) { + return true; + } + // No existing queue found - try to start a new one for promotion + if (StartNewQueue(nBalanceNeedsAnonymized, connman, nSmallerDenom, /*fPromotion=*/true, /*fDemotion=*/false)) { + return true; + } + } + + // Check if we should demote larger -> smaller + if (m_clientman.ShouldDemote(nLargerDenom, nSmallerDenom, denomCounts)) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::DoAutomaticDenominating -- Demotion opportunity: 1 x %s -> %d x %s\n", + CoinJoin::DenominationToString(nLargerDenom), + CoinJoin::PROMOTION_RATIO, + CoinJoin::DenominationToString(nSmallerDenom)); + + // Try to join an existing queue for demotion + if (JoinExistingQueue(nBalanceNeedsAnonymized, connman, nSmallerDenom, /*fPromotion=*/false, /*fDemotion=*/true)) { + return true; + } + // No existing queue found - try to start a new one for demotion + if (StartNewQueue(nBalanceNeedsAnonymized, connman, nSmallerDenom, /*fPromotion=*/false, /*fDemotion=*/true)) { + return true; + } + } + } } - // If we were unable to find/join an existing queue then start a new one. - if (StartNewQueue(nBalanceNeedsAnonymized, connman)) return true; + // Standard mixing only makes sense while there is still balance to anonymize; a + // rebalance-only pass (target reached) must not fall through to standard queues + if (nBalanceNeedsAnonymized > 0) { + // Always attempt to join an existing queue + if (JoinExistingQueue(nBalanceNeedsAnonymized, connman)) { + return true; + } + + // If we were unable to find/join an existing queue then start a new one. + if (StartNewQueue(nBalanceNeedsAnonymized, connman)) return true; + } strAutoDenomResult = _("No compatible Masternode found."); return false; @@ -1106,11 +1232,11 @@ bool CCoinJoinClientManager::DoAutomaticDenominating(ChainstateManager& chainman int nThreshold_low = nThreshold_high * 0.7; size_t used_count{m_mn_metaman.GetUsedMasternodesCount()}; - WalletCJLogPrint(m_wallet, "Checking threshold - used: %d, threshold: %d\n", (int)used_count, nThreshold_high); + WalletCJLogPrint(m_wallet, "Checking threshold - used: %d, threshold: %d\n", static_cast(used_count), nThreshold_high); - if ((int)used_count > nThreshold_high) { + if (static_cast(used_count) > nThreshold_high) { m_mn_metaman.RemoveUsedMasternodes(used_count - nThreshold_low); - WalletCJLogPrint(m_wallet, " new used: %d, threshold: %d\n", (int)m_mn_metaman.GetUsedMasternodesCount(), + WalletCJLogPrint(m_wallet, " new used: %d, threshold: %d\n", static_cast(m_mn_metaman.GetUsedMasternodesCount()), nThreshold_high); } @@ -1177,16 +1303,106 @@ static int WinnersToSkip() ? 1 : 8; } -bool CCoinJoinClientSession::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman) +bool CCoinJoinClientSession::SelectRebalanceInputs(int nTargetDenom, bool fPromotion, std::vector& vecTxDSInRet) +{ + vecTxDSInRet.clear(); + m_vecRebalanceInputs.clear(); + + if (fPromotion) { + // Promotion: select 10 fully-mixed coins of the smaller denomination + auto vecCoins = m_wallet->SelectFullyMixedForPromotion(nTargetDenom, CoinJoin::PROMOTION_RATIO); + if (static_cast(vecCoins.size()) < CoinJoin::PROMOTION_RATIO) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- Not enough fully-mixed coins for promotion\n", __func__); + return false; + } + // Convert COutPoints to CTxDSIn + LOCK(m_wallet->cs_wallet); + for (const auto& outpoint : vecCoins) { + const auto it = m_wallet->mapWallet.find(outpoint.hash); + if (it == m_wallet->mapWallet.end()) continue; + const wallet::CWalletTx& wtx = it->second; + if (outpoint.n >= wtx.tx->vout.size()) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- invalid outpoint index %u for tx %s\n", __func__, outpoint.n, outpoint.hash.ToString()); + continue; + } + vecTxDSInRet.emplace_back(CTxIn(outpoint), wtx.tx->vout[outpoint.n].scriptPubKey, + m_wallet->GetRealOutpointCoinJoinRounds(outpoint)); + } + if (static_cast(vecTxDSInRet.size()) < CoinJoin::PROMOTION_RATIO) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- Failed to build promotion inputs\n", __func__); + vecTxDSInRet.clear(); + return false; + } + } else { + // Demotion: select 1 fully-mixed coin of the larger adjacent denomination. Like + // promotion inputs, the coin's history must already be protected: the demotion's + // 1:10 shape publicly clusters its outputs, which therefore start mixing over + // (see GetRealOutpointCoinJoinRounds), and only a fully-mixed input is worth that. + const int nLargerDenom = CoinJoin::GetLargerAdjacentDenom(nTargetDenom); + if (nLargerDenom == 0) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- No larger adjacent denom for demotion\n", __func__); + return false; + } + if (!m_wallet->SelectTxDSInsByDenomination(nLargerDenom, CoinJoin::DenominationToAmount(nLargerDenom), vecTxDSInRet, CoinType::ONLY_FULLY_MIXED)) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- Couldn't find coin for demotion\n", __func__); + return false; + } + // Keep only 1 input for demotion + vecTxDSInRet.resize(1); + } + + // Lock the selected coins immediately to prevent races with other concurrent CoinJoin + // sessions and record them so UnlockCoins() releases them on any failure path + LOCK(m_wallet->cs_wallet); + for (const auto& txdsin : vecTxDSInRet) { + m_wallet->LockCoin(txdsin.prevout); + m_vecRebalanceInputs.push_back(txdsin.prevout); + vecOutPointLocked.push_back(txdsin.prevout); + } + return true; +} + +void CCoinJoinClientSession::UnlockRebalanceInputs() +{ + if (m_vecRebalanceInputs.empty()) return; + { + LOCK(m_wallet->cs_wallet); + for (const auto& outpoint : m_vecRebalanceInputs) { + m_wallet->UnlockCoin(outpoint); + } + } + vecOutPointLocked.erase(std::remove_if(vecOutPointLocked.begin(), vecOutPointLocked.end(), + [&](const COutPoint& outpoint) { + return std::find(m_vecRebalanceInputs.begin(), m_vecRebalanceInputs.end(), + outpoint) != m_vecRebalanceInputs.end(); + }), + vecOutPointLocked.end()); + m_vecRebalanceInputs.clear(); +} + +bool CCoinJoinClientSession::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman, + int nTargetDenom, bool fPromotion, bool fDemotion) { if (!CCoinJoinClientOptions::IsEnabled()) return false; + // Promotion and demotion are mutually exclusive + assert(!(fPromotion && fDemotion)); + + // For promotion/demotion select and lock the inputs up front: the selection only depends + // on the target denomination, not on the queue, so there is no need to re-select per queue + std::vector vecRebalanceTxDSIn; + const bool fRebalance = (fPromotion || fDemotion) && nTargetDenom != 0; + if (fRebalance && !SelectRebalanceInputs(nTargetDenom, fPromotion, vecRebalanceTxDSIn)) { + strAutoDenomResult = _("Can't mix: no compatible inputs found!"); + return false; + } + const auto mnList = m_dmnman.GetListAtChainTip(); const int nWeightedMnCount = mnList.GetCounts().m_valid_weighted; // Look through the queues and see if anything matches CCoinJoinQueue dsq; - while (m_clientman.GetQueueItemAndTry(dsq)) { + while (m_clientman.GetQueueItemAndTry(dsq, fRebalance ? nTargetDenom : 0)) { auto dmn = mnList.GetValidMNByCollateral(dsq.masternodeOutpoint); if (!dmn) { @@ -1208,10 +1424,12 @@ bool CCoinJoinClientSession::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, std::vector vecTxDSInTmp; - // Try to match their denominations if possible, select exact number of denominations - if (!m_wallet->SelectTxDSInsByDenomination(dsq.nDenom, nBalanceNeedsAnonymized, vecTxDSInTmp)) { - WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::JoinExistingQueue -- Couldn't match denomination %d (%s)\n", dsq.nDenom, CoinJoin::DenominationToString(dsq.nDenom)); - continue; + if (!fRebalance) { + // Standard mixing: try to match their denominations if possible + if (!m_wallet->SelectTxDSInsByDenomination(dsq.nDenom, nBalanceNeedsAnonymized, vecTxDSInTmp)) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::JoinExistingQueue -- Couldn't match denomination %d (%s)\n", dsq.nDenom, CoinJoin::DenominationToString(dsq.nDenom)); + continue; + } } m_mn_metaman.AddUsedMasternode(dmn->proTxHash); @@ -1224,17 +1442,30 @@ bool CCoinJoinClientSession::JoinExistingQueue(CAmount nBalanceNeedsAnonymized, nSessionDenom = dsq.nDenom; mixingMasternode = dmn; - pendingDsaRequest = CPendingDsaRequest(dmn->proTxHash, CCoinJoinAccept(nSessionDenom, txMyCollateral)); + // Declare which side of the session denomination we will occupy so the masternode can + // tell whether the session can still cover both sides (rebalance sessions only) + const uint8_t nDsaFlags = fPromotion ? CCoinJoinAccept::FLAG_PROMOTION + : fDemotion ? CCoinJoinAccept::FLAG_DEMOTION : uint8_t{0}; + pendingDsaRequest = CPendingDsaRequest(dmn->proTxHash, + CCoinJoinAccept(nSessionDenom, txMyCollateral, nDsaFlags)); connman.AddPendingMasternode(dmn->proTxHash); SetState(POOL_STATE_QUEUE); nTimeLastSuccessfulStep = GetTime(); - WalletCJLogPrint(m_wallet, /* Continued */ - "CCoinJoinClientSession::JoinExistingQueue -- pending connection, masternode=%s, nSessionDenom=%d (%s)\n", - dmn->proTxHash.ToString(), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom)); + // Set promotion/demotion session state; the rebalance inputs (if any) were already + // selected, locked and recorded by SelectRebalanceInputs above + m_fPromotion = fPromotion; + m_fDemotion = fDemotion; + if (!fRebalance) m_vecRebalanceInputs.clear(); + + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::JoinExistingQueue -- pending %s connection, masternode=%s, nSessionDenom=%d (%s), %d inputs\n", + fPromotion ? "PROMOTION" : fDemotion ? "DEMOTION" : "mixing", + dmn->proTxHash.ToString(), nSessionDenom.load(), CoinJoin::DenominationToString(nSessionDenom), + vecTxDSInTmp.size() + m_vecRebalanceInputs.size()); strAutoDenomResult = _("Trying to connect…"); return true; } strAutoDenomResult = _("Failed to find mixing queue to join"); + UnlockRebalanceInputs(); return false; } @@ -1310,9 +1541,11 @@ bool CCoinJoinClientSession::StartNewQueue(CAmount nBalanceNeedsAnonymized, CCon pendingDsaRequest = CPendingDsaRequest(dmn->proTxHash, CCoinJoinAccept(nSessionDenom, txMyCollateral)); SetState(POOL_STATE_QUEUE); nTimeLastSuccessfulStep = GetTime(); - WalletCJLogPrint( /* Continued */ - m_wallet, "CCoinJoinClientSession::StartNewQueue -- pending connection, masternode=%s, nSessionDenom=%d (%s)\n", - dmn->proTxHash.ToString(), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom)); + WalletCJLogPrint(/* Continued */ + m_wallet, + "CCoinJoinClientSession::StartNewQueue -- pending connection, masternode=%s, nSessionDenom=%d " + "(%s)\n", + dmn->proTxHash.ToString(), nSessionDenom.load(), CoinJoin::DenominationToString(nSessionDenom)); strAutoDenomResult = _("Trying to connect…"); return true; } @@ -1320,6 +1553,95 @@ bool CCoinJoinClientSession::StartNewQueue(CAmount nBalanceNeedsAnonymized, CCon return false; } +bool CCoinJoinClientSession::StartNewQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman, + int nTargetDenom, bool fPromotion, bool fDemotion) +{ + assert(m_mn_metaman.IsValid()); + + if (!CCoinJoinClientOptions::IsEnabled()) return false; + if (nTargetDenom == 0) return false; + + // Promotion and demotion are mutually exclusive, and this overload needs one of them + assert(!(fPromotion && fDemotion)); + if (!fPromotion && !fDemotion) return false; + + // For promotion/demotion, verify we have the required coins before starting a queue. + // SelectRebalanceInputs locks them and records them in vecOutPointLocked/m_vecRebalanceInputs. + std::vector vecTxDSInTmp; + if (!SelectRebalanceInputs(nTargetDenom, fPromotion, vecTxDSInTmp)) { + return false; + } + + int nTries = 0; + const auto mnList = m_dmnman.GetListAtChainTip(); + const auto mnCounts = mnList.GetCounts(); + const int nMnCount = mnCounts.enabled(); + const int nWeightedMnCount = mnCounts.m_valid_weighted; + + while (nTries < 10) { + auto dmn = GetRandomNotUsedMasternode(); + if (!dmn) { + strAutoDenomResult = _("Can't find random Masternode."); + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::StartNewQueue -- %s\n", strAutoDenomResult.original); + UnlockRebalanceInputs(); + return false; + } + + m_mn_metaman.AddUsedMasternode(dmn->proTxHash); + + // skip next mn payments winners + if (dmn->pdmnState->nLastPaidHeight + nWeightedMnCount < mnList.GetHeight() + WinnersToSkip()) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::StartNewQueue -- skipping winner, masternode=%s\n", dmn->proTxHash.ToString()); + nTries++; + continue; + } + + if (m_mn_metaman.IsMixingThresholdExceeded(dmn->proTxHash, nMnCount)) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::StartNewQueue -- too early to mix with node masternode=%s\n", + dmn->proTxHash.ToString()); + nTries++; + continue; + } + + if (connman.IsMasternodeOrDisconnectRequested(dmn->pdmnState->netInfo->GetPrimary())) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::StartNewQueue -- skipping connection, masternode=%s\n", + dmn->proTxHash.ToString()); + nTries++; + continue; + } + + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::StartNewQueue -- attempting %s connection, masternode=%s, tries=%d\n", + fPromotion ? "PROMOTION" : "DEMOTION", dmn->proTxHash.ToString(), nTries); + + nSessionDenom = nTargetDenom; + mixingMasternode = dmn; + connman.AddPendingMasternode(dmn->proTxHash); + // This overload always starts a promotion/demotion session - declare which side of the + // session denomination we will occupy so the masternode can track session coverage + pendingDsaRequest = CPendingDsaRequest(dmn->proTxHash, + CCoinJoinAccept(nSessionDenom, txMyCollateral, + fPromotion ? CCoinJoinAccept::FLAG_PROMOTION + : CCoinJoinAccept::FLAG_DEMOTION)); + SetState(POOL_STATE_QUEUE); + nTimeLastSuccessfulStep = GetTime(); + + // Store promotion/demotion state; the inputs were already selected, locked and + // recorded in m_vecRebalanceInputs by SelectRebalanceInputs above + m_fPromotion = fPromotion; + m_fDemotion = fDemotion; + + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::StartNewQueue -- pending %s connection, masternode=%s, nSessionDenom=%d (%s), %zu inputs\n", + fPromotion ? "PROMOTION" : "DEMOTION", + dmn->proTxHash.ToString(), nSessionDenom.load(), CoinJoin::DenominationToString(nSessionDenom), + m_vecRebalanceInputs.size()); + strAutoDenomResult = _("Trying to connect…"); + return true; + } + strAutoDenomResult = _("Failed to start a new mixing queue"); + UnlockRebalanceInputs(); + return false; +} + bool CCoinJoinClientSession::ProcessPendingDsaRequest(CConnman& connman) { if (!pendingDsaRequest) return false; @@ -1330,11 +1652,21 @@ bool CCoinJoinClientSession::ProcessPendingDsaRequest(CConnman& connman) } else { WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- cannot find address to connect, masternode=%s\n", __func__, pendingDsaRequest.GetProTxHash().ToString()); + UnlockCoins(); WITH_LOCK(cs_coinjoin, SetNull()); return false; } - bool fDone = connman.ForNode(mn_addr, [this, &connman](CNode* pnode) { + bool fMnTooOldForRebalance{false}; + bool fDone = connman.ForNode(mn_addr, [this, &connman, &fMnTooOldForRebalance](CNode* pnode) { + // Post-V24: a rebalance dsa carries a version-gated flags field the masternode must + // understand - and the masternode must be able to host a rebalance-capable session. + // If it negotiated an older protocol, abort and let DoAutomaticDenominating retry + // with another masternode instead of silently mixing without a reserved slot. + if (pendingDsaRequest.GetDSA().IsRebalance() && pnode->GetCommonVersion() < COINJOIN_REBALANCE_VERSION) { + fMnTooOldForRebalance = true; + return false; + } WalletCJLogPrint(m_wallet, "-- processing dsa queue for addr=%s\n", pnode->addr.ToStringAddrPort()); nTimeLastSuccessfulStep = GetTime(); CNetMsgMaker msgMaker(pnode->GetCommonVersion()); @@ -1342,11 +1674,20 @@ bool CCoinJoinClientSession::ProcessPendingDsaRequest(CConnman& connman) return true; }); + if (fMnTooOldForRebalance) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- masternode too old for a rebalance session, masternode=%s\n", + __func__, pendingDsaRequest.GetProTxHash().ToString()); + UnlockCoins(); + WITH_LOCK(cs_coinjoin, SetNull()); + return false; + } + if (fDone) { pendingDsaRequest = CPendingDsaRequest(); } else if (pendingDsaRequest.IsExpired()) { WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- failed to connect, masternode=%s\n", __func__, pendingDsaRequest.GetProTxHash().ToString()); + UnlockCoins(); WITH_LOCK(cs_coinjoin, SetNull()); } @@ -1390,9 +1731,9 @@ bool CCoinJoinClientManager::MarkAlreadyJoinedQueueAsTried(CCoinJoinQueue& dsq) return false; } -bool CCoinJoinClientManager::GetQueueItemAndTry(CCoinJoinQueue& dsq) const +bool CCoinJoinClientManager::GetQueueItemAndTry(CCoinJoinQueue& dsq, int nDenomFilter) const { - return m_queueman && m_queueman->GetQueueItemAndTry(dsq); + return m_queueman && m_queueman->GetQueueItemAndTry(dsq, nDenomFilter); } bool CCoinJoinClientSession::SubmitDenominate(CConnman& connman) @@ -1400,9 +1741,31 @@ bool CCoinJoinClientSession::SubmitDenominate(CConnman& connman) LOCK(m_wallet->cs_wallet); std::string strError; - std::vector vecTxDSIn; std::vector > vecPSInOutPairsTmp; + // Post-V24: Handle promotion/demotion entries + if (m_fPromotion || m_fDemotion) { + const bool fPrepared = m_fPromotion ? PreparePromotionEntry(strError, vecPSInOutPairsTmp) + : PrepareDemotionEntry(strError, vecPSInOutPairsTmp); + if (fPrepared) { + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::SubmitDenominate -- %s entry prepared, sending\n", + m_fPromotion ? "Promotion" : "Demotion"); + return SendDenominate(vecPSInOutPairsTmp, connman); + } + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::SubmitDenominate -- Prepare%sEntry failed: %s\n", + m_fPromotion ? "Promotion" : "Demotion", strError); + strAutoDenomResult = Untranslated(strError); + // The rebalance inputs were locked back when the session was queued; release them and + // reset the session right away instead of keeping them locked until CheckTimeout() + UnlockCoins(); + keyHolderStorage.ReturnAll(); + WITH_LOCK(cs_coinjoin, SetNull()); + return false; + } + + // Standard 1:1 mixing + std::vector vecTxDSIn; + if (!SelectDenominate(strError, vecTxDSIn)) { WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::SubmitDenominate -- SelectDenominate failed, error: %s\n", strError); return false; @@ -1424,7 +1787,7 @@ bool CCoinJoinClientSession::SubmitDenominate(CConnman& connman) return a.second > b.second || (a.second == b.second && a.first < b.first); }); - WalletCJLogPrint(m_wallet, "vecInputsByRounds for denom %d\n", nSessionDenom); + WalletCJLogPrint(m_wallet, "vecInputsByRounds for denom %d\n", nSessionDenom.load()); for (const auto& pair : vecInputsByRounds) { WalletCJLogPrint(m_wallet, "vecInputsByRounds: rounds: %d, inputs: %d\n", pair.first, pair.second); } @@ -1529,6 +1892,131 @@ bool CCoinJoinClientSession::PrepareDenominate(int nMinRounds, int nMaxRounds, s return true; } +bool CCoinJoinClientSession::PreparePromotionEntry(std::string& strErrorRet, std::vector>& vecPSInOutPairsRet) +{ + AssertLockHeld(m_wallet->cs_wallet); + + vecPSInOutPairsRet.clear(); + + if (m_vecRebalanceInputs.size() != static_cast(CoinJoin::PROMOTION_RATIO)) { + strErrorRet = strprintf("Invalid promotion input count: %d (expected %d)", m_vecRebalanceInputs.size(), CoinJoin::PROMOTION_RATIO); + return false; + } + + // Session denom is the smaller denom (inputs), get the larger adjacent denom for output + const int nLargerDenom = CoinJoin::GetLargerAdjacentDenom(nSessionDenom); + if (nLargerDenom == 0) { + strErrorRet = "No larger adjacent denomination for promotion"; + return false; + } + const CAmount nLargerAmount = CoinJoin::DenominationToAmount(nLargerDenom); + + // Create 10 inputs from stored promotion inputs + for (const auto& outpoint : m_vecRebalanceInputs) { + const auto it = m_wallet->mapWallet.find(outpoint.hash); + if (it == m_wallet->mapWallet.end()) { + strErrorRet = "Promotion input not found in wallet"; + return false; + } + const wallet::CWalletTx& wtx = it->second; + if (outpoint.n >= wtx.tx->vout.size()) { + strErrorRet = "Invalid promotion input index"; + return false; + } + + // Validate the UTXO is still spendable + if (m_wallet->IsSpent(outpoint)) { + strErrorRet = "Promotion input has been spent"; + return false; + } + + CTxDSIn txdsin(CTxIn(outpoint), wtx.tx->vout[outpoint.n].scriptPubKey, + m_wallet->GetRealOutpointCoinJoinRounds(outpoint)); + + // Pair every input with an empty placeholder output - SendDenominate filters + // empty outputs, so only the real larger-denom output set below is submitted + vecPSInOutPairsRet.emplace_back(txdsin, CTxOut()); + } + + // Set the single real output (larger denomination) on the last pair + CScript scriptDenom = keyHolderStorage.AddKey(m_wallet.get()); + if (!vecPSInOutPairsRet.empty()) { + vecPSInOutPairsRet.back().second = CTxOut(nLargerAmount, scriptDenom); + } + + // NOTE: all inputs were locked and recorded in vecOutPointLocked by SelectRebalanceInputs + + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::PreparePromotionEntry -- Prepared %d inputs for promotion to %s\n", + vecPSInOutPairsRet.size(), CoinJoin::DenominationToString(nLargerDenom)); + + return true; +} + +bool CCoinJoinClientSession::PrepareDemotionEntry(std::string& strErrorRet, std::vector>& vecPSInOutPairsRet) +{ + AssertLockHeld(m_wallet->cs_wallet); + + vecPSInOutPairsRet.clear(); + + if (m_vecRebalanceInputs.size() != 1) { + strErrorRet = strprintf("Invalid demotion input count: %d (expected 1)", m_vecRebalanceInputs.size()); + return false; + } + + // Session denom is the smaller denom (outputs) + const CAmount nSmallerAmount = CoinJoin::DenominationToAmount(nSessionDenom); + const int nLargerDenom = CoinJoin::GetLargerAdjacentDenom(nSessionDenom); + if (nLargerDenom == 0) { + strErrorRet = "No larger adjacent denomination for demotion"; + return false; + } + + // Get the single input (larger denom) + const COutPoint& outpoint = m_vecRebalanceInputs[0]; + const auto it = m_wallet->mapWallet.find(outpoint.hash); + if (it == m_wallet->mapWallet.end()) { + strErrorRet = "Demotion input not found in wallet"; + return false; + } + const wallet::CWalletTx& wtx = it->second; + if (outpoint.n >= wtx.tx->vout.size()) { + strErrorRet = "Invalid demotion input index"; + return false; + } + + // Validate the UTXO is still spendable + if (m_wallet->IsSpent(outpoint)) { + strErrorRet = "Demotion input has been spent"; + return false; + } + + CTxDSIn txdsin(CTxIn(outpoint), wtx.tx->vout[outpoint.n].scriptPubKey, + m_wallet->GetRealOutpointCoinJoinRounds(outpoint)); + + // Create 10 outputs of smaller denomination + // For demotion: 1 input, 10 outputs + // The first pair has the real input, subsequent pairs have empty inputs + for (int i = 0; i < CoinJoin::PROMOTION_RATIO; ++i) { + CScript scriptDenom = keyHolderStorage.AddKey(m_wallet.get()); + CTxOut txout(nSmallerAmount, scriptDenom); + + if (i == 0) { + // First entry has the real input + vecPSInOutPairsRet.emplace_back(txdsin, txout); + } else { + // Subsequent entries have empty inputs (will be filtered out when building entry) + vecPSInOutPairsRet.emplace_back(CTxDSIn(), txout); + } + } + + // NOTE: the input was locked and recorded in vecOutPointLocked by SelectRebalanceInputs + + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::PrepareDemotionEntry -- Prepared 1 input for demotion to %d x %s\n", + CoinJoin::PROMOTION_RATIO, CoinJoin::DenominationToString(nSessionDenom)); + + return true; +} + // Create collaterals by looping through inputs grouped by addresses bool CCoinJoinClientSession::MakeCollateralAmounts() { @@ -1789,7 +2277,7 @@ bool CCoinJoinClientSession::CreateDenominated(CAmount nBalanceToDenominate, con if (fAddFinal && nBalanceToDenominate > 0 && nBalanceToDenominate < nDenomValue) { fAddFinal = false; // add final denom only once, only the smalest possible one WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 1 - FINAL - nDenomValue: %f, nBalanceToDenominate: %f, nOutputs: %d, %s\n", - strFunc, (float) nDenomValue / COIN, (float) nBalanceToDenominate / COIN, nOutputs, txBuilder.ToString()); + strFunc, static_cast(nDenomValue) / COIN, static_cast(nBalanceToDenominate) / COIN, nOutputs, txBuilder.ToString()); return true; } else if (nBalanceToDenominate >= nDenomValue) { return true; @@ -1806,10 +2294,10 @@ bool CCoinJoinClientSession::CreateDenominated(CAmount nBalanceToDenominate, con ++currentDenomIt->second; nBalanceToDenominate -= nDenomValue; WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 1 - nDenomValue: %f, nBalanceToDenominate: %f, nOutputs: %d, %s\n", - __func__, (float) nDenomValue / COIN, (float) nBalanceToDenominate / COIN, nOutputs, txBuilder.ToString()); + __func__, static_cast(nDenomValue) / COIN, static_cast(nBalanceToDenominate) / COIN, nOutputs, txBuilder.ToString()); } else { WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 1 - Error: AddOutput failed for nDenomValue: %f, nBalanceToDenominate: %f, nOutputs: %d, %s\n", - __func__, (float) nDenomValue / COIN, (float) nBalanceToDenominate / COIN, nOutputs, txBuilder.ToString()); + __func__, static_cast(nDenomValue) / COIN, static_cast(nBalanceToDenominate) / COIN, nOutputs, txBuilder.ToString()); return false; } @@ -1825,11 +2313,11 @@ bool CCoinJoinClientSession::CreateDenominated(CAmount nBalanceToDenominate, con if (count < CCoinJoinClientOptions::GetDenomsGoal() && txBuilder.CouldAddOutput(denom) && nBalanceToDenominate > 0) { finished = false; WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 1 - NOT finished - nDenomValue: %f, count: %d, nBalanceToDenominate: %f, %s\n", - __func__, (float) denom / COIN, count, (float) nBalanceToDenominate / COIN, txBuilder.ToString()); + __func__, static_cast(denom) / COIN, count, static_cast(nBalanceToDenominate) / COIN, txBuilder.ToString()); break; } WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 1 - FINISHED - nDenomValue: %f, count: %d, nBalanceToDenominate: %f, %s\n", - __func__, (float) denom / COIN, count, (float) nBalanceToDenominate / COIN, txBuilder.ToString()); + __func__, static_cast(denom) / COIN, count, static_cast(nBalanceToDenominate) / COIN, txBuilder.ToString()); } if (finished) break; @@ -1874,7 +2362,7 @@ bool CCoinJoinClientSession::CreateDenominated(CAmount nBalanceToDenominate, con // Use the smaller value int denomsToCreate = denomsToCreateValue > denomsToCreateBal ? denomsToCreateBal : denomsToCreateValue; WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 2 - nBalanceToDenominate: %f, nDenomValue: %f, denomsToCreateValue: %d, denomsToCreateBal: %d\n", - __func__, (float) nBalanceToDenominate / COIN, (float) nDenomValue / COIN, denomsToCreateValue, denomsToCreateBal); + __func__, static_cast(nBalanceToDenominate) / COIN, static_cast(nDenomValue) / COIN, denomsToCreateValue, denomsToCreateBal); auto it = mapDenomCount.find(nDenomValue); for (const auto i : util::irange(denomsToCreate)) { // Never go above the cap unless it's the largest denom @@ -1890,17 +2378,17 @@ bool CCoinJoinClientSession::CreateDenominated(CAmount nBalanceToDenominate, con break; } WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 2 - nDenomValue: %f, nBalanceToDenominate: %f, nOutputs: %d, %s\n", - __func__, (float) nDenomValue / COIN, (float) nBalanceToDenominate / COIN, nOutputs, txBuilder.ToString()); + __func__, static_cast(nDenomValue) / COIN, static_cast(nBalanceToDenominate) / COIN, nOutputs, txBuilder.ToString()); if (txBuilder.CountOutputs() >= COINJOIN_DENOM_OUTPUTS_THRESHOLD) break; } if (txBuilder.CountOutputs() >= COINJOIN_DENOM_OUTPUTS_THRESHOLD) break; } } - WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 3 - nBalanceToDenominate: %f, %s\n", __func__, (float) nBalanceToDenominate / COIN, txBuilder.ToString()); + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 3 - nBalanceToDenominate: %f, %s\n", __func__, static_cast(nBalanceToDenominate) / COIN, txBuilder.ToString()); for (const auto& [denom, count] : mapDenomCount) { - WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 3 - DONE - nDenomValue: %f, count: %d\n", __func__, (float) denom / COIN, count); + WalletCJLogPrint(m_wallet, "CCoinJoinClientSession::%s -- 3 - DONE - nDenomValue: %f, count: %d\n", __func__, static_cast(denom) / COIN, count); } // No reasons to create mixing collaterals if we can't create denoms to mix @@ -2002,3 +2490,31 @@ UniValue CCoinJoinClientManager::getJsonInfo() const return obj; } +bool CCoinJoinClientManager::ShouldPromote(int nSmallerDenom, int nLargerDenom, const wallet::CoinJoinDenomCounts& counts) +{ + // Validate denominations are adjacent + if (!CoinJoin::AreAdjacentDenominations(nSmallerDenom, nLargerDenom)) { + return false; + } + + const int idxSmaller = CoinJoin::GetDenominationIndex(nSmallerDenom); + const int idxLarger = CoinJoin::GetDenominationIndex(nLargerDenom); + + return CoinJoin::ShouldPromoteDenoms(counts.total[idxSmaller], counts.total[idxLarger], + counts.fully_mixed[idxSmaller], CCoinJoinClientOptions::GetDenomsGoal()); +} + +bool CCoinJoinClientManager::ShouldDemote(int nLargerDenom, int nSmallerDenom, const wallet::CoinJoinDenomCounts& counts) +{ + // Validate denominations are adjacent + if (!CoinJoin::AreAdjacentDenominations(nLargerDenom, nSmallerDenom)) { + return false; + } + + const int idxLarger = CoinJoin::GetDenominationIndex(nLargerDenom); + const int idxSmaller = CoinJoin::GetDenominationIndex(nSmallerDenom); + + return CoinJoin::ShouldDemoteDenoms(counts.total[idxLarger], counts.total[idxSmaller], + counts.fully_mixed[idxLarger], CCoinJoinClientOptions::GetDenomsGoal()); +} + diff --git a/src/coinjoin/client.h b/src/coinjoin/client.h index 30eaa0d9dbe4..0bc68d893501 100644 --- a/src/coinjoin/client.h +++ b/src/coinjoin/client.h @@ -89,6 +89,11 @@ class CCoinJoinClientSession : public CCoinJoinBaseSession CKeyHolderStorage keyHolderStorage; // storage for keys used in PrepareDenominate + // Post-V24: Promotion/demotion session state + bool m_fPromotion{false}; // True if this session is promoting smaller -> larger denom + bool m_fDemotion{false}; // True if this session is demoting larger -> smaller denom + std::vector m_vecRebalanceInputs; // Selected inputs for promotion/demotion rebalancing + /// Create denominations bool CreateDenominated(CAmount nBalanceToDenominate); bool CreateDenominated(CAmount nBalanceToDenominate, const wallet::CompactTallyItem& tallyItem, bool fCreateMixingCollaterals) @@ -102,16 +107,35 @@ class CCoinJoinClientSession : public CCoinJoinBaseSession bool CreateCollateralTransaction(CMutableTransaction& txCollateral, std::string& strReason) EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet); - bool JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman); + bool JoinExistingQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman, + int nTargetDenom = 0, bool fPromotion = false, bool fDemotion = false); bool StartNewQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman); + bool StartNewQueue(CAmount nBalanceNeedsAnonymized, CConnman& connman, + int nTargetDenom, bool fPromotion, bool fDemotion); CDeterministicMNCPtr GetRandomNotUsedMasternode(); + /// Post-V24: select and lock inputs for a promotion/demotion session. Locked outpoints are + /// recorded in m_vecRebalanceInputs and vecOutPointLocked so UnlockCoins() releases them on + /// any failure path. + bool SelectRebalanceInputs(int nTargetDenom, bool fPromotion, std::vector& vecTxDSInRet); + /// Post-V24: unlock and forget the inputs selected by SelectRebalanceInputs (session setup failed) + void UnlockRebalanceInputs(); + /// step 0: select denominated inputs and txouts bool SelectDenominate(std::string& strErrorRet, std::vector& vecTxDSInRet); /// step 1: prepare denominated inputs and outputs bool PrepareDenominate(int nMinRounds, int nMaxRounds, std::string& strErrorRet, const std::vector& vecTxDSIn, std::vector>& vecPSInOutPairsRet, bool fDryRun = false) EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet); + + /// Post-V24: prepare promotion entry (10 inputs of smaller denom -> 1 output of larger denom) + bool PreparePromotionEntry(std::string& strErrorRet, std::vector>& vecPSInOutPairsRet) + EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet); + + /// Post-V24: prepare demotion entry (1 input of larger denom -> 10 outputs of smaller denom) + bool PrepareDemotionEntry(std::string& strErrorRet, std::vector>& vecPSInOutPairsRet) + EXCLUSIVE_LOCKS_REQUIRED(m_wallet->cs_wallet); + /// step 2: send denominated inputs and outputs prepared in step 1 bool SendDenominate(const std::vector >& vecPSInOutPairsIn, CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); @@ -226,7 +250,7 @@ class CCoinJoinClientManager : public interfaces::CoinJoin::Client bool TrySubmitDenominate(const uint256& proTxHash, CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions); bool MarkAlreadyJoinedQueueAsTried(CCoinJoinQueue& dsq) const EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions); - bool GetQueueItemAndTry(CCoinJoinQueue& dsq) const; + bool GetQueueItemAndTry(CCoinJoinQueue& dsq, int nDenomFilter = 0) const; void CheckTimeout() EXCLUSIVE_LOCKS_REQUIRED(!cs_deqsessions); @@ -256,6 +280,24 @@ class CCoinJoinClientManager : public interfaces::CoinJoin::Client bool isMixing() const override; bool startMixing() override; void stopMixing() override; + + /** + * Post-V24: Check if we should promote smaller denominations into larger ones + * @param nSmallerDenom The smaller denomination to promote from + * @param nLargerDenom The larger denomination to promote into + * @param counts Wallet denomination counts, e.g. from CWallet::GetDenominationCounts() + * @return true if promotion is recommended + */ + static bool ShouldPromote(int nSmallerDenom, int nLargerDenom, const wallet::CoinJoinDenomCounts& counts); + + /** + * Post-V24: Check if we should demote larger denominations into smaller ones + * @param nLargerDenom The larger denomination to demote from + * @param nSmallerDenom The smaller denomination to demote into + * @param counts Wallet denomination counts, e.g. from CWallet::GetDenominationCounts() + * @return true if demotion is recommended + */ + static bool ShouldDemote(int nLargerDenom, int nSmallerDenom, const wallet::CoinJoinDenomCounts& counts); }; #endif // BITCOIN_COINJOIN_CLIENT_H diff --git a/src/coinjoin/coinjoin.cpp b/src/coinjoin/coinjoin.cpp index 3711dbc89f01..5f104b853e71 100644 --- a/src/coinjoin/coinjoin.cpp +++ b/src/coinjoin/coinjoin.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -88,24 +89,69 @@ bool CCoinJoinBroadcastTx::CheckSignature(const CBLSPublicKey& blsPubKey) const return true; } -bool CCoinJoinBroadcastTx::IsValidStructure() const +bool CCoinJoinBroadcastTx::IsExpired(const CBlockIndex* pindex, const chainlock::Chainlocks& clhandler) const { - // some trivial checks only + // expire confirmed DSTXes after ~1h since confirmation or chainlocked confirmation + if (!nConfirmedHeight.has_value() || pindex->nHeight < *nConfirmedHeight) return false; // not mined yet + if (pindex->nHeight - *nConfirmedHeight > 24) return true; // mined more than an hour ago + return clhandler.HasChainLock(pindex->nHeight, *pindex->phashBlock); +} + +bool CCoinJoinBroadcastTx::IsValidStructure(const CBlockIndex* pindex, const ChainstateManager& chainman, + bool* fPossiblyValidPostV24Ret) const +{ + if (fPossiblyValidPostV24Ret) *fPossiblyValidPostV24Ret = false; + + // some trivial checks only, activation-independent ones first if (masternodeOutpoint.IsNull() && m_protxHash.IsNull()) { return false; } - if (tx->vin.size() != tx->vout.size()) { + + if (tx->vin.size() < static_cast(CoinJoin::GetMinPoolParticipants())) { return false; } - if (tx->vin.size() < size_t(CoinJoin::GetMinPoolParticipants())) { + + if (!std::ranges::all_of(tx->vout, [](const auto& txOut) { + return CoinJoin::IsDenominatedAmount(txOut.nValue) && txOut.scriptPubKey.IsPayToPublicKeyHash(); + })) { return false; } - if (tx->vin.size() > CoinJoin::GetMaxPoolInputOutputCount()) { - return false; + + // Post-V24: allow unbalanced counts (promotion/demotion) and up to 200 inputs + // (20 participants * 10 inputs for promotions) + // Pre-V24: require balanced input/output counts (1:1 mixing only), max 180 inputs + // (20 participants * 9 entries) + // Note: For post-V24 unbalanced transactions (promotion/demotion), value sum validation + // (inputs == outputs) requires UTXO access and is performed in IsValidInOuts() when the + // transaction is processed. + const size_t nMaxInputsPreV24 = CoinJoin::GetMaxPoolParticipants() * COINJOIN_ENTRY_MAX_SIZE; + const size_t nMaxInOutsPostV24 = CoinJoin::GetMaxPoolParticipants() * CoinJoin::PROMOTION_RATIO; + const bool fValidPreV24 = tx->vin.size() == tx->vout.size() && tx->vin.size() <= nMaxInputsPreV24; + // Post-V24 the input/output counts no longer bound each other one-to-one, but they still + // bound each other: an all-promotion transaction carries at most PROMOTION_RATIO inputs per + // output and an all-demotion one the mirror image, so neither side may exceed PROMOTION_RATIO + // times the other. Beyond that, each promotion contributes PROMOTION_RATIO - 1 more inputs + // than outputs and each demotion the mirror, while standard entries contribute equally to + // both sides, so the difference between the sides of a transaction composable from valid + // entries is always a multiple of that step. + const size_t nSideDiff = tx->vin.size() > tx->vout.size() ? tx->vin.size() - tx->vout.size() + : tx->vout.size() - tx->vin.size(); + const bool fValidPostV24 = tx->vin.size() <= nMaxInOutsPostV24 && tx->vout.size() <= nMaxInOutsPostV24 && + tx->vin.size() <= tx->vout.size() * static_cast(CoinJoin::PROMOTION_RATIO) && + tx->vout.size() <= tx->vin.size() * static_cast(CoinJoin::PROMOTION_RATIO) && + nSideDiff % static_cast(CoinJoin::PROMOTION_RATIO - 1) == 0; + + const bool fV24Active = pindex && DeploymentActiveAt(*pindex, chainman, Consensus::DEPLOYMENT_V24); + if (fV24Active) { + return fValidPostV24; } - return std::ranges::all_of(tx->vout, [](const auto& txOut) { - return CoinJoin::IsDenominatedAmount(txOut.nValue) && txOut.scriptPubKey.IsPayToPublicKeyHash(); - }); + + // Pre-V24: report whether the tx would be valid on a post-V24 tip so callers can avoid + // fully punishing relayers whose tip is ahead of ours around the activation boundary + if (!fValidPreV24 && fValidPostV24 && fPossiblyValidPostV24Ret) { + *fPossiblyValidPostV24Ret = true; + } + return fValidPreV24; } void CCoinJoinBaseSession::SetNull() @@ -170,7 +216,7 @@ bool CoinJoinQueueManager::TryAddQueue(CCoinJoinQueue dsq) return true; } -bool CoinJoinQueueManager::GetQueueItemAndTry(CCoinJoinQueue& dsqRet) +bool CoinJoinQueueManager::GetQueueItemAndTry(CCoinJoinQueue& dsqRet, int nDenomFilter) { TRY_LOCK(cs_vecqueue, lockDS); if (!lockDS) return false; // it's ok to fail here, we run this quite frequently @@ -178,6 +224,8 @@ bool CoinJoinQueueManager::GetQueueItemAndTry(CCoinJoinQueue& dsqRet) for (auto& dsq : vecCoinJoinQueue) { // only try each queue once if (dsq.fTried || dsq.IsTimeOutOfBounds()) continue; + // skip before marking as tried: a queue we never looked at must stay available + if (nDenomFilter != 0 && dsq.nDenom != nDenomFilter) continue; dsq.fTried = true; dsqRet = dsq; return true; @@ -204,26 +252,91 @@ std::string CCoinJoinBaseSession::GetStateString() const } } +bool CoinJoin::IsPromotionDemotionActive(const ChainstateManager& chainman, bool fNextBlock) +{ + LOCK(::cs_main); + const CBlockIndex* pindex = chainman.ActiveChain().Tip(); + if (pindex == nullptr) return false; + return fNextBlock ? DeploymentActiveAfter(pindex, chainman, Consensus::DEPLOYMENT_V24) + : DeploymentActiveAt(*pindex, chainman, Consensus::DEPLOYMENT_V24); +} + bool CCoinJoinBaseSession::IsValidInOuts(Chainstate& active_chainstate, const llmq::CInstantSendManager& isman, const CTxMemPool& mempool, const std::vector& vin, - const std::vector& vout, PoolMessage& nMessageIDRet, - bool* fConsumeCollateralRet) const + const std::vector& vout, int session_denom, bool fAllowRebalanceShapes, + PoolMessage& nMessageIDRet, bool* fConsumeCollateralRet, bool fFinalTx, + CoinJoin::SessionDenomCounts* pDenomCountsRet) { std::set setScripPubKeys; nMessageIDRet = MSG_NOERR; if (fConsumeCollateralRet) *fConsumeCollateralRet = false; - if (vin.size() != vout.size()) { + // Determine entry type based on input/output counts + // Standard: N inputs, N outputs (same denom) + // Promotion: PROMOTION_RATIO inputs of session denom, 1 output of larger adjacent denom + // Demotion: 1 input of larger adjacent denom, PROMOTION_RATIO outputs of session denom + // FinalTx: aggregate of the above across all participants - per-entry shapes don't apply + enum class EntryType { STANDARD, PROMOTION, DEMOTION, FINAL_TX, INVALID }; + EntryType entryType = EntryType::STANDARD; + + if (fFinalTx && fAllowRebalanceShapes) { + entryType = EntryType::FINAL_TX; + } else if (vin.size() == vout.size()) { + entryType = EntryType::STANDARD; + } else if (fAllowRebalanceShapes) { + if (vin.size() == static_cast(CoinJoin::PROMOTION_RATIO) && vout.size() == 1) { + entryType = EntryType::PROMOTION; + } else if (vin.size() == 1 && vout.size() == static_cast(CoinJoin::PROMOTION_RATIO)) { + entryType = EntryType::DEMOTION; + } else { + entryType = EntryType::INVALID; + } + } else { + // Pre-V24: only standard entries allowed LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: inputs vs outputs size mismatch! %d vs %d\n", __func__, vin.size(), vout.size()); nMessageIDRet = ERR_SIZE_MISMATCH; if (fConsumeCollateralRet) *fConsumeCollateralRet = true; return false; } - auto checkTxOut = [&](const CTxOut& txout) { - if (int nDenom = CoinJoin::AmountToDenomination(txout.nValue); nDenom != nSessionDenom) { - LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: incompatible denom %d (%s) != nSessionDenom %d (%s)\n", - nDenom, CoinJoin::DenominationToString(nDenom), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom)); + if (entryType == EntryType::INVALID) { + LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: invalid entry structure! %d inputs, %d outputs\n", __func__, vin.size(), vout.size()); + nMessageIDRet = ERR_SIZE_MISMATCH; + if (fConsumeCollateralRet) *fConsumeCollateralRet = true; + return false; + } + + // Validate promotion/demotion entries using dedicated validators + // and determine expected denominations for UTXO input validation + int nExpectedInputDenom = session_denom; + int nExpectedOutputDenom = session_denom; + // Final tx only: promotion outputs and demotion inputs use the larger adjacent + // denomination, so it is allowed in addition to the session denomination. + // 0 (never matched) for per-entry validation or when session_denom is the largest denom. + int nLargerDenom{0}; + + if (entryType == EntryType::PROMOTION) { + if (!CoinJoin::ValidatePromotionEntry(vin, vout, session_denom, nMessageIDRet)) { + if (fConsumeCollateralRet) *fConsumeCollateralRet = true; + return false; + } + nExpectedOutputDenom = CoinJoin::GetLargerAdjacentDenom(session_denom); + } else if (entryType == EntryType::DEMOTION) { + if (!CoinJoin::ValidateDemotionEntry(vin, vout, session_denom, nMessageIDRet)) { + if (fConsumeCollateralRet) *fConsumeCollateralRet = true; + return false; + } + nExpectedInputDenom = CoinJoin::GetLargerAdjacentDenom(session_denom); + } else if (entryType == EntryType::FINAL_TX) { + nLargerDenom = CoinJoin::GetLargerAdjacentDenom(session_denom); + } + + auto checkTxOut = [&](const CTxOut& txout, int nExpectedDenom) { + const int nDenom = CoinJoin::AmountToDenomination(txout.nValue); + + if (nDenom != nExpectedDenom && (nLargerDenom == 0 || nDenom != nLargerDenom)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: incompatible denom %d (%s) != expected %d (%s)\n", + nDenom, CoinJoin::DenominationToString(nDenom), nExpectedDenom, CoinJoin::DenominationToString(nExpectedDenom)); nMessageIDRet = ERR_DENOM; if (fConsumeCollateralRet) *fConsumeCollateralRet = true; return false; @@ -234,23 +347,27 @@ bool CCoinJoinBaseSession::IsValidInOuts(Chainstate& active_chainstate, const ll if (fConsumeCollateralRet) *fConsumeCollateralRet = true; return false; } + // Check for duplicate scripts across all inputs and outputs (privacy requirement) if (!setScripPubKeys.insert(txout.scriptPubKey).second) { LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: already have this script! scriptPubKey=%s\n", ScriptToAsmStr(txout.scriptPubKey)); nMessageIDRet = ERR_ALREADY_HAVE; if (fConsumeCollateralRet) *fConsumeCollateralRet = true; return false; } - // IsPayToPublicKeyHash() above already checks for scriptPubKey size, - // no need to double-check, hence no usage of ERR_NON_STANDARD_PUBKEY return true; }; CAmount nFees{0}; + size_t nLargerInputs{0}; + size_t nLargerOutputs{0}; for (const auto& txout : vout) { - if (!checkTxOut(txout)) { + if (!checkTxOut(txout, nExpectedOutputDenom)) { return false; } + if (nLargerDenom != 0 && CoinJoin::AmountToDenomination(txout.nValue) == nLargerDenom) { + ++nLargerOutputs; + } nFees -= txout.nValue; } @@ -274,21 +391,46 @@ bool CCoinJoinBaseSession::IsValidInOuts(Chainstate& active_chainstate, const ll return false; } - if (!checkTxOut(coin.out)) { + if (!checkTxOut(coin.out, nExpectedInputDenom)) { return false; } + if (nLargerDenom != 0 && CoinJoin::AmountToDenomination(coin.out.nValue) == nLargerDenom) { + ++nLargerInputs; + } + nFees += coin.out.nValue; } - // The same size and denom for inputs and outputs ensures their total value is also the same, - // no need to double-check. If not, we are doing something wrong, bail out. + // Value sum must match: inputs == outputs (no fees in CoinJoin) + // This holds for standard mixing (same denom) and promotion/demotion (value preserved) if (nFees != 0) { LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: non-zero fees! fees: %lld\n", __func__, nFees); nMessageIDRet = ERR_FEES; return false; } + if (entryType == EntryType::FINAL_TX && + !CoinJoin::ValidateFinalTxComposition(vin.size(), vout.size(), nLargerInputs, nLargerOutputs)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: inconsistent final tx composition! %d/%d total, %d/%d larger denom inputs/outputs\n", + __func__, vin.size(), vout.size(), nLargerInputs, nLargerOutputs); + nMessageIDRet = ERR_SIZE_MISMATCH; + return false; + } + + if (pDenomCountsRet) { + pDenomCountsRet->inputs = vin.size() - nLargerInputs; + pDenomCountsRet->outputs = vout.size() - nLargerOutputs; + } + + LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- Valid %s entry: %d inputs, %d outputs\n", + __func__, + entryType == EntryType::PROMOTION ? "PROMOTION" : + entryType == EntryType::DEMOTION ? "DEMOTION" : + entryType == EntryType::FINAL_TX ? "FINAL_TX" : + "STANDARD", + vin.size(), vout.size()); + return true; } @@ -435,11 +577,7 @@ CCoinJoinBroadcastTx CDSTXManager::GetDSTX(const uint256& hash) bool CDSTXManager::IsTxExpired(const CCoinJoinBroadcastTx& tx, const CBlockIndex* pindex) const { - // expire confirmed DSTXes after ~1h since confirmation or chainlocked - const auto& opt_confirmed_height = tx.GetConfirmedHeight(); - if (!opt_confirmed_height.has_value() || pindex->nHeight < *opt_confirmed_height) return false; // not mined yet - return (pindex->nHeight - *opt_confirmed_height > 24) || - m_chainlocks.HasChainLock(pindex->nHeight, *pindex->phashBlock); // mined more than an hour ago or chainlocked + return tx.IsExpired(pindex, m_chainlocks); } void CDSTXManager::CheckDSTXes(const CBlockIndex* pindex) @@ -512,3 +650,111 @@ void CDSTXManager::BlockDisconnected(const std::shared_ptr& pblock int CoinJoin::GetMinPoolParticipants() { return Params().PoolMinParticipants(); } int CoinJoin::GetMaxPoolParticipants() { return Params().PoolMaxParticipants(); } + +bool CoinJoin::ValidatePromotionEntry(const std::vector& vecTxIn, const std::vector& vecTxOut, + int nSessionDenom, PoolMessage& nMessageIDRet) +{ + // Promotion: 10 inputs of smaller denom → 1 output of larger denom + // Session denom is the smaller denom (inputs) + nMessageIDRet = MSG_NOERR; + + // Check input count + if (vecTxIn.size() != static_cast(PROMOTION_RATIO)) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: wrong input count %zu, expected %d\n", + vecTxIn.size(), PROMOTION_RATIO); + nMessageIDRet = ERR_SIZE_MISMATCH; + return false; + } + + // Check output count + if (vecTxOut.size() != 1) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: wrong output count %zu, expected 1\n", + vecTxOut.size()); + nMessageIDRet = ERR_SIZE_MISMATCH; + return false; + } + + // Get the larger adjacent denomination + const int nLargerDenom = GetLargerAdjacentDenom(nSessionDenom); + if (nLargerDenom == 0) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: no larger adjacent denom for %s\n", + DenominationToString(nSessionDenom)); + nMessageIDRet = ERR_DENOM; + return false; + } + + // Validate output is at larger denomination + const int nOutputDenom = AmountToDenomination(vecTxOut[0].nValue); + if (nOutputDenom != nLargerDenom) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: output denom %s != expected %s\n", + DenominationToString(nOutputDenom), DenominationToString(nLargerDenom)); + nMessageIDRet = ERR_DENOM; + return false; + } + + // Validate output is P2PKH + if (!vecTxOut[0].scriptPubKey.IsPayToPublicKeyHash()) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidatePromotionEntry -- ERROR: output is not P2PKH\n"); + nMessageIDRet = ERR_INVALID_SCRIPT; + return false; + } + + return true; +} + +bool CoinJoin::ValidateDemotionEntry(const std::vector& vecTxIn, const std::vector& vecTxOut, + int nSessionDenom, PoolMessage& nMessageIDRet) +{ + // Demotion: 1 input of larger denom → 10 outputs of smaller denom + // Session denom is the smaller denom (outputs) + nMessageIDRet = MSG_NOERR; + + // Check input count + if (vecTxIn.size() != 1) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: wrong input count %zu, expected 1\n", + vecTxIn.size()); + nMessageIDRet = ERR_SIZE_MISMATCH; + return false; + } + + // Check output count + if (vecTxOut.size() != static_cast(PROMOTION_RATIO)) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: wrong output count %zu, expected %d\n", + vecTxOut.size(), PROMOTION_RATIO); + nMessageIDRet = ERR_SIZE_MISMATCH; + return false; + } + + // Validate all outputs are at session denomination and P2PKH + for (const auto& txout : vecTxOut) { + const int nDenom = AmountToDenomination(txout.nValue); + if (nDenom != nSessionDenom) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: output denom %s != session denom %s\n", + DenominationToString(nDenom), DenominationToString(nSessionDenom)); + nMessageIDRet = ERR_DENOM; + return false; + } + if (!txout.scriptPubKey.IsPayToPublicKeyHash()) { + LogPrint(BCLog::COINJOIN, "CoinJoin::ValidateDemotionEntry -- ERROR: output is not P2PKH\n"); + nMessageIDRet = ERR_INVALID_SCRIPT; + return false; + } + } + + return true; +} + +bool CoinJoin::ValidateFinalTxComposition(size_t nTotalInputs, size_t nTotalOutputs, + size_t nLargerInputs, size_t nLargerOutputs) +{ + // Aggregate consistency: every larger-denom output consumes PROMOTION_RATIO session-denom + // inputs (promotion) and every larger-denom input produces PROMOTION_RATIO session-denom + // outputs (demotion); the remainder on both sides are standard 1:1 inputs/outputs and + // must not be negative. Together with the zero-fee check in IsValidInOuts() this + // guarantees the final tx is composable from valid standard/promotion/demotion entries. + if (nLargerInputs > nTotalInputs || nLargerOutputs > nTotalOutputs) return false; + const size_t nSessionInputs = nTotalInputs - nLargerInputs; + const size_t nSessionOutputs = nTotalOutputs - nLargerOutputs; + return nSessionInputs >= nLargerOutputs * size_t(PROMOTION_RATIO) && + nSessionOutputs >= nLargerInputs * size_t(PROMOTION_RATIO); +} diff --git a/src/coinjoin/coinjoin.h b/src/coinjoin/coinjoin.h index 0cc205bdcf66..637fdaec0bcf 100644 --- a/src/coinjoin/coinjoin.h +++ b/src/coinjoin/coinjoin.h @@ -60,7 +60,7 @@ int GetMinPoolParticipants(); int GetMaxPoolParticipants(); /// Maximum number of inputs or outputs across a full pool -inline size_t GetMaxPoolInputOutputCount() { return size_t(GetMaxPoolParticipants()) * COINJOIN_ENTRY_MAX_SIZE; } +inline size_t GetMaxPoolInputOutputCount() { return static_cast(GetMaxPoolParticipants()) * COINJOIN_ENTRY_MAX_SIZE; } } // namespace CoinJoin // pool responses @@ -139,23 +139,43 @@ class CCoinJoinStatusUpdate class CCoinJoinAccept { public: + //! dsa flags (post-V24): which kind of rebalance entry this participant intends to submit. + //! The direction matters to the masternode because a promotion only ever adds coins at the + //! session denomination to the input side and a demotion only to the output side, and each + //! side needs either nobody or at least two participants (see CoinJoin::MixSideCounts). + static constexpr uint8_t FLAG_PROMOTION{1 << 0}; + static constexpr uint8_t FLAG_DEMOTION{1 << 1}; + int nDenom{0}; CMutableTransaction txCollateral; + //! Only serialized between peers at or above COINJOIN_REBALANCE_VERSION; old peers + //! neither send nor receive it, so their wire format is unchanged + uint8_t nFlags{0}; CCoinJoinAccept() = default; - CCoinJoinAccept(int nDenom, CMutableTransaction txCollateral) : + CCoinJoinAccept(int nDenom, CMutableTransaction txCollateral, uint8_t nFlags = 0) : nDenom(nDenom), - txCollateral(std::move(txCollateral)){}; + txCollateral(std::move(txCollateral)), + nFlags(nFlags){}; SERIALIZE_METHODS(CCoinJoinAccept, obj) { READWRITE(obj.nDenom, obj.txCollateral); + if (s.GetVersion() >= COINJOIN_REBALANCE_VERSION) { + READWRITE(obj.nFlags); + } } + [[nodiscard]] bool IsPromotion() const { return (nFlags & FLAG_PROMOTION) != 0; } + [[nodiscard]] bool IsDemotion() const { return (nFlags & FLAG_DEMOTION) != 0; } + [[nodiscard]] bool IsRebalance() const { return IsPromotion() || IsDemotion(); } + //! A participant mixes in exactly one direction; anything else is malformed + [[nodiscard]] bool HasValidFlags() const { return !(IsPromotion() && IsDemotion()); } + friend bool operator==(const CCoinJoinAccept& a, const CCoinJoinAccept& b) { - return a.nDenom == b.nDenom && CTransaction(a.txCollateral) == CTransaction(b.txCollateral); + return a.nDenom == b.nDenom && a.nFlags == b.nFlags && CTransaction(a.txCollateral) == CTransaction(b.txCollateral); } }; @@ -203,6 +223,21 @@ class CCoinJoinEntry } bool AddScriptSig(const CTxIn& txin); + + /// Which side(s) of the session denomination this entry occupies, derived from its shape. + /// Empty and malformed entries are UNKNOWN; they occupy neither side and are rejected + /// before they reach the pool. + [[nodiscard]] CoinJoin::MixShape GetMixShape() const + { + using CoinJoin::MixShape; + if (vecTxDSIn.empty() || vecTxOut.empty()) return MixShape::UNKNOWN; + if (vecTxDSIn.size() == vecTxOut.size()) return MixShape::STANDARD; + if (vecTxDSIn.size() == static_cast(CoinJoin::PROMOTION_RATIO) && vecTxOut.size() == 1) return MixShape::PROMOTION; + if (vecTxDSIn.size() == 1 && vecTxOut.size() == static_cast(CoinJoin::PROMOTION_RATIO)) return MixShape::DEMOTION; + return MixShape::UNKNOWN; + } + + [[nodiscard]] bool IsStandardMixingEntry() const { return GetMixShape() == CoinJoin::MixShape::STANDARD; } }; @@ -317,7 +352,15 @@ class CCoinJoinBroadcastTx [[nodiscard]] const std::optional& GetConfirmedHeight() const { return nConfirmedHeight; } void SetConfirmedHeight(std::optional nConfirmedHeightIn) { assert(nConfirmedHeightIn == std::nullopt || *nConfirmedHeightIn > 0); nConfirmedHeight = nConfirmedHeightIn; } - [[nodiscard]] bool IsValidStructure() const; + [[nodiscard]] bool IsExpired(const CBlockIndex* pindex, const chainlock::Chainlocks& clhandler) const; + /** + * Trivial structural checks against the V24-dependent rules at pindex. When the tx fails + * only because V24 is not active at pindex but would be structurally valid on a post-V24 + * tip, fPossiblyValidPostV24Ret (if provided) is set to true so callers can tolerate tip + * skew around the activation boundary. + */ + [[nodiscard]] bool IsValidStructure(const CBlockIndex* pindex, const ChainstateManager& chainman, + bool* fPossiblyValidPostV24Ret = nullptr) const; }; // base class @@ -337,12 +380,29 @@ class CCoinJoinBaseSession virtual void SetNull() EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); - bool IsValidInOuts(Chainstate& active_chainstate, const llmq::CInstantSendManager& isman, - const CTxMemPool& mempool, const std::vector& vin, const std::vector& vout, - PoolMessage& nMessageIDRet, bool* fConsumeCollateralRet) const; + /** + * Validate inputs/outputs of a single mixing entry or, with fFinalTx=true, of the + * aggregated final transaction. Post-V24 a final transaction concatenates standard (N:N), + * promotion (10:1) and demotion (1:10) entries, so per-entry shape rules don't apply to it; + * aggregate rules (allowed denominations, value balance, input/output consistency) are + * checked instead. session_denom is the caller's snapshot of the session denomination. + * + * fAllowRebalanceShapes tells us whether promotion/demotion shapes are permitted here. It is + * passed in rather than derived from the tip so that it stays fixed for the lifetime of a + * session: the masternode passes the session's own capability and the client the + * boundary-tolerant tip check, and neither can be flipped mid-session by a reorg across the + * V24 boundary. + */ + static bool IsValidInOuts(Chainstate& active_chainstate, const llmq::CInstantSendManager& isman, + const CTxMemPool& mempool, const std::vector& vin, const std::vector& vout, + int session_denom, bool fAllowRebalanceShapes, PoolMessage& nMessageIDRet, + bool* fConsumeCollateralRet, bool fFinalTx = false, + CoinJoin::SessionDenomCounts* pDenomCountsRet = nullptr); public: - int nSessionDenom{0}; // Users must submit a denom matching this + // Atomic because the message-handling and scheduler threads write it while those threads and + // RPC callers also read it without holding cs_coinjoin. + std::atomic nSessionDenom{0}; CCoinJoinBaseSession() = default; virtual ~CCoinJoinBaseSession() = default; @@ -352,6 +412,21 @@ class CCoinJoinBaseSession int GetEntriesCount() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin) { LOCK(cs_coinjoin); return vecEntries.size(); } int GetEntriesCountLocked() const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin) { return vecEntries.size(); } + + /// Participants occupying each side of the session denomination among the entries received + CoinJoin::MixSideCounts GetMixSideCounts() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin) + { + LOCK(cs_coinjoin); + return GetMixSideCountsLocked(); + } + CoinJoin::MixSideCounts GetMixSideCountsLocked() const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin) + { + CoinJoin::MixSideCounts counts; + for (const auto& entry : vecEntries) { + counts.Add(entry.GetMixShape()); + } + return counts; + } }; class CoinJoinQueueManager @@ -369,7 +444,9 @@ class CoinJoinQueueManager void CheckQueue() EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue); int GetQueueSize() const EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue) { LOCK(cs_vecqueue); return vecCoinJoinQueue.size(); } - bool GetQueueItemAndTry(CCoinJoinQueue& dsqRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue); + //! nDenomFilter != 0 restricts the search to that denomination. Queues it skips are left + //! untried, so a rebalance attempt doesn't consume the announcements standard mixing needs. + bool GetQueueItemAndTry(CCoinJoinQueue& dsqRet, int nDenomFilter = 0) EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue); bool HasQueue(const uint256& queueHash) EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue) { @@ -415,9 +492,52 @@ namespace CoinJoin constexpr CAmount GetMaxPoolAmount() { return COINJOIN_ENTRY_MAX_SIZE * vecStandardDenominations.front(); } + /// Whether denomination promotion/demotion (post-V24) is active at the current chain tip. + /// With fNextBlock, also counts a deployment that activates in the block following the tip: + /// the peer that sent us a rebalance-shaped transaction may be one block ahead of us at the + /// activation boundary, and treating its transaction as malformed would cost us either a + /// discouraged peer or, when signing, our collateral. + bool IsPromotionDemotionActive(const ChainstateManager& chainman, bool fNextBlock = false); + /// If the collateral is valid given by a client bool IsCollateralValid(ChainstateManager& chainman, const llmq::CInstantSendManager& isman, const CTxMemPool& mempool, const CTransaction& txCollateral); + + /** + * Validate a promotion entry: 10 inputs of smaller denom → 1 output of larger denom + * @param vecTxIn The inputs for this entry + * @param vecTxOut The outputs for this entry + * @param nSessionDenom The session denomination (the smaller denom for promotion) + * @param nMessageIDRet Error message if validation fails + * @return true if valid promotion entry + */ + bool ValidatePromotionEntry(const std::vector& vecTxIn, const std::vector& vecTxOut, + int nSessionDenom, PoolMessage& nMessageIDRet); + + /** + * Validate a demotion entry: 1 input of larger denom → 10 outputs of smaller denom + * @param vecTxIn The inputs for this entry + * @param vecTxOut The outputs for this entry + * @param nSessionDenom The session denomination (the smaller denom for demotion outputs) + * @param nMessageIDRet Error message if validation fails + * @return true if valid demotion entry + */ + bool ValidateDemotionEntry(const std::vector& vecTxIn, const std::vector& vecTxOut, + int nSessionDenom, PoolMessage& nMessageIDRet); + + /** + * Check that a final transaction's aggregate input/output counts are composable from valid + * standard (N:N), promotion (PROMOTION_RATIO:1) and demotion (1:PROMOTION_RATIO) entries. + * Inputs/outputs at the larger adjacent denomination are counted separately from the + * session-denomination remainder; value balance is checked by the caller. + * @param nTotalInputs Total number of inputs in the final tx + * @param nTotalOutputs Total number of outputs in the final tx + * @param nLargerInputs Number of inputs at the larger adjacent denomination (demotions) + * @param nLargerOutputs Number of outputs at the larger adjacent denomination (promotions) + * @return true if the counts are consistent with a mix of valid entries + */ + bool ValidateFinalTxComposition(size_t nTotalInputs, size_t nTotalOutputs, + size_t nLargerInputs, size_t nLargerOutputs); } class CDSTXManager diff --git a/src/coinjoin/common.h b/src/coinjoin/common.h index 3d97d639434d..3a3b3177bc90 100644 --- a/src/coinjoin/common.h +++ b/src/coinjoin/common.h @@ -10,7 +10,9 @@ #include #include +#include #include +#include #include /** Holds a mixing input @@ -47,6 +49,21 @@ constexpr std::array vecStandardDenominations{ constexpr std::array GetStandardDenominations() { return vecStandardDenominations; } constexpr CAmount GetSmallestDenomination() { return vecStandardDenominations.back(); } +/** + * Get the index of a denomination in vecStandardDenominations (0=largest, 4=smallest) + * Returns -1 if not a valid denomination + */ +constexpr int GetDenominationIndex(int nDenom) +{ + if (nDenom <= 0) return -1; + for (size_t i = 0; i < vecStandardDenominations.size(); ++i) { + if (nDenom == (1 << i)) { + return static_cast(i); + } + } + return -1; +} + /* Return a bitshifted integer representing a denomination in vecStandardDenominations or 0 if none was found @@ -74,9 +91,7 @@ constexpr CAmount DenominationToAmount(int nDenom) return 0; } - size_t nMaxDenoms = vecStandardDenominations.size(); - - if (nDenom >= (1 << nMaxDenoms) || nDenom < 0) { + if (nDenom >= (1 << vecStandardDenominations.size()) || nDenom < 0) { // out of bounds return -1; } @@ -86,16 +101,8 @@ constexpr CAmount DenominationToAmount(int nDenom) return -2; } - CAmount nDenomAmount{-3}; - - for (size_t i = 0; i < nMaxDenoms; ++i) { - if (nDenom & (1 << i)) { - nDenomAmount = vecStandardDenominations[i]; - break; - } - } - - return nDenomAmount; + const int idx = GetDenominationIndex(nDenom); + return idx >= 0 ? vecStandardDenominations[idx] : -3; } @@ -110,6 +117,139 @@ std::string DenominationToString(int nDenom); constexpr CAmount GetCollateralAmount() { return GetSmallestDenomination() / 10; } constexpr CAmount GetMaxCollateralAmount() { return GetCollateralAmount() * 4; } +// Promotion/demotion constants (post-V24 feature) +constexpr int PROMOTION_RATIO = 10; // 10 smaller denomination coins = 1 larger denomination coin +constexpr int GAP_DIVISOR = 5; // Deficit gap required to trigger promotion/demotion, as 1/N of the goal + +/** + * How far behind the other denomination a denomination has to be before converting is worth it; + * the gap is what keeps promotion and demotion from oscillating. It is a fraction of the goal + * rather than a constant because the largest gap two denominations can show is the goal itself: + * a constant of 10 was unsatisfiable at MIN_COINJOIN_DENOMS_GOAL (also 10) and silently + * disabled the feature there. At the default goal of 50 this still yields 10. + */ +constexpr int GetGapThreshold(int nGoal) { return std::max(nGoal / GAP_DIVISOR, 1); } + +/** + * Which side(s) of the session denomination a participant occupies. A standard entry mixes at + * the session denomination on both sides. A promotion spends PROMOTION_RATIO session-denom + * inputs for one larger-denom output, so it only occupies the input side; a demotion is the + * mirror image. UNKNOWN is for empty or malformed entries, which occupy neither. + */ +enum class MixShape : uint8_t { UNKNOWN, STANDARD, PROMOTION, DEMOTION }; + +/** + * How many participants occupy each side of the session denomination. + * + * Mixing only conceals a participant when someone else holds coins of the same size on the + * same side: a group of session-denom coins is hidden by the other session-denom coins it + * could be confused with. A participant holding a single coin at the larger denomination has + * no group to hide, which is why only the session denomination is counted here. + */ +struct MixSideCounts { + int inputs{0}; //!< participants contributing session-denom inputs (standard + promotions) + int outputs{0}; //!< participants receiving session-denom outputs (standard + demotions) + + constexpr void Add(MixShape shape) + { + switch (shape) { + case MixShape::STANDARD: + ++inputs; + ++outputs; + break; + case MixShape::PROMOTION: + ++inputs; + break; + case MixShape::DEMOTION: + ++outputs; + break; + case MixShape::UNKNOWN: + break; + } + } + + /** + * Each side must be occupied by nobody or by at least two participants. Exactly one is the + * only forbidden count: that participant's session-denom coins would be the only ones of + * their size on that side and so would be trivially identifiable on-chain. + */ + [[nodiscard]] constexpr bool IsCovered() const { return inputs != 1 && outputs != 1; } +}; + +/// How many coins of a final transaction sit at the session denomination on each side +struct SessionDenomCounts { + size_t inputs{0}; + size_t outputs{0}; +}; + +/** + * Check if two denominations are adjacent (one step apart in the denom list) + * Used for validating promotion/demotion entries post-V24 + */ +constexpr bool AreAdjacentDenominations(int nDenom1, int nDenom2) +{ + int idx1 = GetDenominationIndex(nDenom1); + int idx2 = GetDenominationIndex(nDenom2); + if (idx1 < 0 || idx2 < 0) return false; + return (idx1 == idx2 + 1) || (idx1 == idx2 - 1); +} + +/** + * Get the larger adjacent denomination (returns 0 if none exists or invalid) + */ +constexpr int GetLargerAdjacentDenom(int nDenom) +{ + int idx = GetDenominationIndex(nDenom); + if (idx <= 0) return 0; // Already largest or invalid + return 1 << (idx - 1); +} + +/** + * Get the smaller adjacent denomination (returns 0 if none exists or invalid) + */ +constexpr int GetSmallerAdjacentDenom(int nDenom) +{ + int idx = GetDenominationIndex(nDenom); + if (idx < 0 || idx >= static_cast(vecStandardDenominations.size()) - 1) return 0; + return 1 << (idx + 1); +} + +/** + * Core promotion decision (post-V24): combine PROMOTION_RATIO fully-mixed coins of the + * smaller denomination into one coin of the larger adjacent denomination when the larger + * denomination is further from the per-denom goal by more than the gap threshold (which + * prevents promote/demote oscillation). Callers resolve wallet counts and validate that + * the denominations are adjacent. + */ +constexpr bool ShouldPromoteDenoms(int nSmallerCount, int nLargerCount, int nSmallerFullyMixedCount, int nGoal) +{ + // Don't sacrifice a denomination that's still being built up + if (nSmallerCount < nGoal / 2) return false; + // A promotion consumes PROMOTION_RATIO fully-mixed coins + if (nSmallerFullyMixedCount < PROMOTION_RATIO) return false; + const int nSmallerDeficit = nSmallerCount < nGoal ? nGoal - nSmallerCount : 0; + const int nLargerDeficit = nLargerCount < nGoal ? nGoal - nLargerCount : 0; + return nLargerDeficit > nSmallerDeficit + GetGapThreshold(nGoal); +} + +/** + * Core demotion decision (post-V24): split one coin of the larger denomination into + * PROMOTION_RATIO coins of the smaller adjacent denomination when the smaller denomination + * is further from the per-denom goal by more than the gap threshold. Like promotion, demotion + * only spends a fully-mixed coin: the conversion's public 1:10 shape clusters its outputs, + * which start mixing over, so only an input with a protected history is worth converting. + */ +constexpr bool ShouldDemoteDenoms(int nLargerCount, int nSmallerCount, int nLargerFullyMixedCount, int nGoal) +{ + // Don't sacrifice a denomination that's still being built up + if (nLargerCount < nGoal / 2) return false; + // A demotion consumes one fully-mixed coin + if (nLargerFullyMixedCount < 1) return false; + const int nSmallerDeficit = nSmallerCount < nGoal ? nGoal - nSmallerCount : 0; + const int nLargerDeficit = nLargerCount < nGoal ? nGoal - nLargerCount : 0; + return nSmallerDeficit > nLargerDeficit + GetGapThreshold(nGoal); +} + constexpr bool IsCollateralAmount(CAmount nInputAmount) { // collateral input can be anything between 1x and "max" (including both) @@ -121,7 +261,7 @@ constexpr int CalculateAmountPriority(CAmount nInputAmount) if (nInputAmount < 0 || nInputAmount > MAX_MONEY) return 0; if (auto optDenom = util::find_if_opt(GetStandardDenominations(), [&nInputAmount](const auto& denom) { return nInputAmount == denom; })) { - return (float)COIN / *optDenom * 10000; + return static_cast(COIN) / *optDenom * 10000; } if (nInputAmount < COIN) { return 20000; diff --git a/src/coinjoin/options.cpp b/src/coinjoin/options.cpp index 87879f50301d..deec629deb00 100644 --- a/src/coinjoin/options.cpp +++ b/src/coinjoin/options.cpp @@ -66,11 +66,11 @@ void CCoinJoinClientOptions::Init() assert(!CCoinJoinClientOptions::_instance); static CCoinJoinClientOptions instance; instance.fCoinJoinMultiSession = gArgs.GetBoolArg("-coinjoinmultisession", DEFAULT_COINJOIN_MULTISESSION); - instance.nCoinJoinSessions = std::min(std::max((int)gArgs.GetIntArg("-coinjoinsessions", DEFAULT_COINJOIN_SESSIONS), MIN_COINJOIN_SESSIONS), MAX_COINJOIN_SESSIONS); - instance.nCoinJoinRounds = std::min(std::max((int)gArgs.GetIntArg("-coinjoinrounds", DEFAULT_COINJOIN_ROUNDS), MIN_COINJOIN_ROUNDS), MAX_COINJOIN_ROUNDS); - instance.nCoinJoinAmount = std::min(std::max((int)gArgs.GetIntArg("-coinjoinamount", DEFAULT_COINJOIN_AMOUNT), MIN_COINJOIN_AMOUNT), MAX_COINJOIN_AMOUNT); - instance.nCoinJoinDenomsGoal = std::min(std::max((int)gArgs.GetIntArg("-coinjoindenomsgoal", DEFAULT_COINJOIN_DENOMS_GOAL), MIN_COINJOIN_DENOMS_GOAL), MAX_COINJOIN_DENOMS_GOAL); - instance.nCoinJoinDenomsHardCap = std::min(std::max((int)gArgs.GetIntArg("-coinjoindenomshardcap", DEFAULT_COINJOIN_DENOMS_HARDCAP), MIN_COINJOIN_DENOMS_HARDCAP), MAX_COINJOIN_DENOMS_HARDCAP); + instance.nCoinJoinSessions = std::min(std::max(static_cast(gArgs.GetIntArg("-coinjoinsessions", DEFAULT_COINJOIN_SESSIONS)), MIN_COINJOIN_SESSIONS), MAX_COINJOIN_SESSIONS); + instance.nCoinJoinRounds = std::min(std::max(static_cast(gArgs.GetIntArg("-coinjoinrounds", DEFAULT_COINJOIN_ROUNDS)), MIN_COINJOIN_ROUNDS), MAX_COINJOIN_ROUNDS); + instance.nCoinJoinAmount = std::min(std::max(static_cast(gArgs.GetIntArg("-coinjoinamount", DEFAULT_COINJOIN_AMOUNT)), MIN_COINJOIN_AMOUNT), MAX_COINJOIN_AMOUNT); + instance.nCoinJoinDenomsGoal = std::min(std::max(static_cast(gArgs.GetIntArg("-coinjoindenomsgoal", DEFAULT_COINJOIN_DENOMS_GOAL)), MIN_COINJOIN_DENOMS_GOAL), MAX_COINJOIN_DENOMS_GOAL); + instance.nCoinJoinDenomsHardCap = std::min(std::max(static_cast(gArgs.GetIntArg("-coinjoindenomshardcap", DEFAULT_COINJOIN_DENOMS_HARDCAP)), MIN_COINJOIN_DENOMS_HARDCAP), MAX_COINJOIN_DENOMS_HARDCAP); CCoinJoinClientOptions::_instance = &instance; } diff --git a/src/coinjoin/server.cpp b/src/coinjoin/server.cpp index 6d9a3ba16578..15d0e9961a5c 100644 --- a/src/coinjoin/server.cpp +++ b/src/coinjoin/server.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -67,7 +68,7 @@ void CCoinJoinServer::ProcessDSACCEPT(CNode& peer, CDataStream& vRecv) { assert(m_mn_metaman.IsValid()); - if (IsSessionReady()) { + if (WITH_LOCK(cs_coinjoin, return IsSessionReady())) { // too many users in this session already, reject new ones LogPrint(BCLog::COINJOIN, "DSACCEPT -- queue is already full!\n"); PushStatus(peer, STATUS_REJECTED, ERR_QUEUE_FULL); @@ -86,7 +87,7 @@ void CCoinJoinServer::ProcessDSACCEPT(CNode& peer, CDataStream& vRecv) return; } - if (vecSessionCollaterals.empty()) { + if (WITH_LOCK(cs_coinjoin, return vecSessionCollaterals.empty())) { { const auto hasQueue = m_queueman.TryHasQueueFromMasternode(m_mn_activeman.GetOutPoint()); if (!hasQueue.has_value()) return; @@ -112,8 +113,8 @@ void CCoinJoinServer::ProcessDSACCEPT(CNode& peer, CDataStream& vRecv) PoolMessage nMessageID = MSG_NOERR; - bool fResult = nSessionID == 0 ? CreateNewSession(dsa, nMessageID) - : AddUserToExistingSession(dsa, nMessageID); + bool fResult = nSessionID == 0 ? CreateNewSession(dsa, peer.GetCommonVersion(), nMessageID) + : AddUserToExistingSession(dsa, peer.GetCommonVersion(), nMessageID); if (fResult) { LogPrint(BCLog::COINJOIN, "DSACCEPT -- is compatible, please submit!\n"); PushStatus(peer, STATUS_ACCEPTED, nMessageID); @@ -200,7 +201,7 @@ void CCoinJoinServer::ProcessDSQUEUE(NodeId from, CDataStream& vRecv) void CCoinJoinServer::ProcessDSVIN(CNode& peer, CDataStream& vRecv) { //do we have enough users in the current session? - if (!IsSessionReady()) { + if (!WITH_LOCK(cs_coinjoin, return IsSessionReady())) { LogPrint(BCLog::COINJOIN, "DSVIN -- session not complete!\n"); PushStatus(peer, STATUS_REJECTED, ERR_SESSION); return; @@ -211,6 +212,9 @@ void CCoinJoinServer::ProcessDSVIN(CNode& peer, CDataStream& vRecv) LogPrint(BCLog::COINJOIN, "DSVIN -- txCollateral %s", entry.txCollateral->ToString()); /* Continued */ + // Note: unbalanced (promotion/demotion) entries are only valid post-V24; AddEntry -> + // IsValidInOuts rejects them pre-V24 and consumes the collateral to keep spam costly + PoolMessage nMessageID = MSG_NOERR; entry.addr = peer.addr; @@ -262,7 +266,7 @@ void CCoinJoinServer::ProcessDSSIGNFINALTX(CNode& peer, CDataStream& vRecv) LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- vecTxIn.size() %s\n", vecTxIn.size()); int nTxInIndex = 0; - int nTxInsCount = (int)vecTxIn.size(); + int nTxInsCount = static_cast(vecTxIn.size()); for (const auto& txin : vecTxIn) { nTxInIndex++; @@ -284,6 +288,9 @@ void CCoinJoinServer::SetNull() // MN side vecSessionCollaterals.clear(); setSessionCollateralPrevouts.clear(); + m_fRebalanceSession = false; + m_fHasLegacyParticipant = false; + m_mapDeclaredShapes.clear(); CCoinJoinBaseSession::SetNull(); m_queueman.SetNull(); @@ -294,42 +301,84 @@ void CCoinJoinServer::SetNull() // void CCoinJoinServer::CheckPool() { - if (int entries = GetEntriesCount(); entries != 0) - LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- entries count %lu\n", entries); + // Every decision below reads several pieces of session state at once, so take them as one + // snapshot. Sampling them separately let the message-handling thread commit an entry + // between two reads: the side counts could be read before the last entry arrived while the + // entry count was read after it, so a session that was about to finalize looked like one + // whose entries were all in but left a side uncovered - and got reset. Reading + // vecSessionCollaterals without the lock could also race SetNull() clearing it. + const auto snap = GetPoolSnapshot(); + + if (snap.entries != 0) + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- entries count %lu\n", snap.entries); + + // PRIVACY: a final transaction is only worth publishing when each side of the session + // denomination is occupied by nobody or by at least two participants; a lone participant + // on a side would have the only coins of that size there and be trivially identifiable. // If we have an entry for each collateral, then create final tx - if (nState == POOL_STATE_ACCEPTING_ENTRIES && size_t(GetEntriesCount()) == vecSessionCollaterals.size()) { - LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- FINALIZE TRANSACTIONS\n"); - CreateFinalTransaction(); + if (snap.state == POOL_STATE_ACCEPTING_ENTRIES && snap.entries == snap.collaterals) { + if (snap.sides.IsCovered()) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- FINALIZE TRANSACTIONS\n"); + CreateFinalTransaction(snap.session_id); + return; + } + // The participant set is frozen once entries are being accepted, so this session can + // never gain the missing counterparty - reset instead of stalling everyone until + // timeout. Shouldn't happen: admission checks the declared shapes up front. + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- all entries received but a session denom side is uncovered (%d in, %d out), resetting session\n", + snap.sides.inputs, snap.sides.outputs); + // Tell the participants before dropping them, so they release their inputs and collateral + // right away instead of waiting out their own timeout. Must precede SetNull(), which + // clears the entries this iterates. + RelayCompletedTransaction(ERR_SESSION); + // Only drop the session the snapshot described; the message-handling thread may have + // reset and restarted one while we were relaying. + WITH_LOCK(cs_coinjoin, if (IsCurrentSession(snap.session_id)) SetNull()); return; } // Check for Time Out // If we timed out while accepting entries, then if we have more than minimum, create final tx - if (nState == POOL_STATE_ACCEPTING_ENTRIES && CCoinJoinServer::HasTimedOut() && - GetEntriesCount() >= CoinJoin::GetMinPoolParticipants()) { + if (snap.state == POOL_STATE_ACCEPTING_ENTRIES && CCoinJoinServer::HasTimedOut() && snap.sides.IsCovered() && + snap.entries >= static_cast(CoinJoin::GetMinPoolParticipants())) { // Punish misbehaving participants ChargeFees(); // Try to complete this session ignoring the misbehaving ones - CreateFinalTransaction(); + CreateFinalTransaction(snap.session_id); return; } // If we have all the signatures, try to compile the transaction - if (nState == POOL_STATE_SIGNING && IsSignaturesComplete()) { + if (snap.state == POOL_STATE_SIGNING && IsSignaturesComplete()) { LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- SIGNING\n"); CommitFinalTransaction(); return; } } -void CCoinJoinServer::CreateFinalTransaction() +CCoinJoinServer::PoolSnapshot CCoinJoinServer::GetPoolSnapshot() const +{ + AssertLockNotHeld(cs_coinjoin); + LOCK(cs_coinjoin); + return PoolSnapshot{nSessionID, nState, vecEntries.size(), vecSessionCollaterals.size(), + GetMixSideCountsLocked()}; +} + +void CCoinJoinServer::CreateFinalTransaction(int session_id) { AssertLockNotHeld(cs_coinjoin); LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- FINALIZE TRANSACTIONS\n"); LOCK(cs_coinjoin); + // The decision to finalize came from a snapshot taken before this lock, so make sure it + // still describes the live session - it may have timed out and been replaced in between. + if (!IsCurrentSession(session_id)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- session changed, not finalizing\n"); + return; + } + CMutableTransaction txNew; // make our new transaction @@ -490,7 +539,15 @@ void CCoinJoinServer::ChargeFees() const */ void CCoinJoinServer::ChargeRandomFees() const { - for (const auto& txCollateral : vecSessionCollaterals) { + AssertLockNotHeld(cs_coinjoin); + + std::vector session_collaterals; + { + LOCK(cs_coinjoin); + session_collaterals = vecSessionCollaterals; + } + + for (const auto& txCollateral : session_collaterals) { if (GetRand(/*nMax=*/100) > 10) return; LogPrint(BCLog::COINJOIN, /* Continued */ "CCoinJoinServer::ChargeRandomFees -- charging random fees, txCollateral=%s", txCollateral->ToString()); @@ -509,6 +566,37 @@ void CCoinJoinServer::ConsumeCollateral(const CTransactionRef& txref) const } } +void CCoinJoinServer::ConsumeCollateralIfCurrentSession(int session_id, const CTransactionRef& txref) const +{ + AssertLockNotHeld(cs_coinjoin); + + // cs_coinjoin is released around the collateral and UTXO checks in AddEntry, so by the time + // we get here the scheduler thread may have timed the session out and started another one. + // Charging then would spend the collateral of a participant that is no longer in any + // session - it may even be a replay of an innocent participant's collateral. + const bool fStillCurrent = + WITH_LOCK(cs_coinjoin, return IsCurrentSession(session_id) && HasSessionCollateral(txref)); + if (!fStillCurrent) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- session changed, not consuming collateral %s\n", __func__, + txref->GetHash().ToString()); + return; + } + ConsumeCollateral(txref); +} + +bool CCoinJoinServer::IsCurrentSession(int session_id) const +{ + AssertLockHeld(cs_coinjoin); + return nSessionID != 0 && nSessionID == session_id && nState == POOL_STATE_ACCEPTING_ENTRIES; +} + +bool CCoinJoinServer::HasSessionCollateral(const CTransactionRef& txref) const +{ + AssertLockHeld(cs_coinjoin); + return std::ranges::any_of(vecSessionCollaterals, + [&txref](const CTransactionRef& ref) { return *ref == *txref; }); +} + bool CCoinJoinServer::HasTimedOut() const { if (nState == POOL_STATE_IDLE) return false; @@ -541,17 +629,23 @@ void CCoinJoinServer::CheckTimeout() */ void CCoinJoinServer::CheckForCompleteQueue() { - if (nState == POOL_STATE_QUEUE && IsSessionReady()) { - SetState(POOL_STATE_ACCEPTING_ENTRIES); + int session_denom; + size_t participants; + { + LOCK(cs_coinjoin); + if (nState != POOL_STATE_QUEUE || !IsSessionReady()) return; - CCoinJoinQueue dsq(nSessionDenom, m_mn_activeman.GetOutPoint(), m_mn_activeman.GetProTxHash(), - GetAdjustedTime(), true); - LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckForCompleteQueue -- queue is ready, signing and relaying (%s) " /* Continued */ - "with %d participants\n", dsq.ToString(), vecSessionCollaterals.size()); - dsq.vchSig = m_mn_activeman.SignBasic(dsq.GetSignatureHash()); - m_peer_manager->PeerRelayDSQ(dsq); - m_queueman.AddQueue(std::move(dsq)); + SetState(POOL_STATE_ACCEPTING_ENTRIES); + session_denom = nSessionDenom; + participants = vecSessionCollaterals.size(); } + + CCoinJoinQueue dsq(session_denom, m_mn_activeman.GetOutPoint(), m_mn_activeman.GetProTxHash(), GetAdjustedTime(), true); + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckForCompleteQueue -- ready queue %s with %d participants\n", + dsq.ToString(), participants); + dsq.vchSig = m_mn_activeman.SignBasic(dsq.GetSignatureHash()); + m_peer_manager->PeerRelayDSQ(dsq); + m_queueman.AddQueue(std::move(dsq)); } // Check to make sure a given input matches an input in the pool and its scriptSig is valid @@ -606,31 +700,75 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag { AssertLockNotHeld(cs_coinjoin); - if (size_t(GetEntriesCount()) >= vecSessionCollaterals.size()) { - LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entries is full!\n", __func__); - nMessageIDRet = ERR_ENTRIES_FULL; - return false; + const auto hasEntryForCollateral = [&entry](const CCoinJoinEntry& other) { + return *other.txCollateral == *entry.txCollateral; + }; + + CoinJoin::MixShape declaredShape{CoinJoin::MixShape::STANDARD}; + int session_denom{0}; + int session_id{0}; + bool fRebalanceSession{false}; + { + LOCK(cs_coinjoin); + + // Entries belong to a session that is actually collecting them. IsSessionReady() was + // checked by the caller before the entry was deserialized, so the state may already + // have moved on by now. + if (nSessionID == 0 || nState != POOL_STATE_ACCEPTING_ENTRIES) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: not accepting entries, nState=%d\n", __func__, + nState.load()); + nMessageIDRet = ERR_SESSION; + return false; + } + + if (static_cast(GetEntriesCountLocked()) >= vecSessionCollaterals.size()) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entries is full!\n", __func__); + nMessageIDRet = ERR_ENTRIES_FULL; + return false; + } + session_denom = nSessionDenom; + session_id = nSessionID; + fRebalanceSession = m_fRebalanceSession; + + // Entries are keyed one-to-one to the collaterals accepted at dsa time: a collateral + // that never went through dsa acceptance cannot submit an entry and an accepted + // collateral covers exactly one entry. Otherwise, once the ready queue is public, + // anyone holding a valid collateral could fill entry slots (and with them the + // declared-shape cover) belonging to the admitted participants. Don't consume the + // collateral in either case - it may be a replay of an innocent participant's + // collateral rather than misbehavior by its owner. + if (!HasSessionCollateral(entry.txCollateral)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: collateral %s was not accepted into this session!\n", + __func__, entry.txCollateral->GetHash().ToString()); + nMessageIDRet = ERR_SESSION; + return false; + } + if (std::ranges::any_of(vecEntries, hasEntryForCollateral)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: already have an entry for collateral %s!\n", + __func__, entry.txCollateral->GetHash().ToString()); + nMessageIDRet = ERR_ALREADY_HAVE; + return false; + } + + if (const auto it = m_mapDeclaredShapes.find(entry.txCollateral->GetHash()); it != m_mapDeclaredShapes.end()) { + declaredShape = it->second; + } } - if (entry.vecTxDSIn.size() > COINJOIN_ENTRY_MAX_SIZE || entry.vecTxOut.size() > COINJOIN_ENTRY_MAX_SIZE) { + // Entry shape rules follow the session, not the current tip: m_fRebalanceSession was fixed + // when the session was created and the participants were admitted under it. Re-deriving them + // from the tip would let a reorg across the V24 boundary retroactively shrink the cap and + // charge an admitted participant for an entry that was legal when it was accepted. + // + // Post-V24: promotion entries carry PROMOTION_RATIO (10) inputs and demotion entries + // PROMOTION_RATIO outputs; pre-V24 entries are capped at COINJOIN_ENTRY_MAX_SIZE (9) + const size_t nMaxEntrySize = fRebalanceSession ? static_cast(CoinJoin::PROMOTION_RATIO) : COINJOIN_ENTRY_MAX_SIZE; + if (entry.vecTxDSIn.size() > nMaxEntrySize || entry.vecTxOut.size() > nMaxEntrySize) { LogPrint(BCLog::COINJOIN, /* Continued */ "CCoinJoinServer::%s -- ERROR: too many inputs or outputs! inputs=%s/%s, outputs=%s/%s\n", __func__, - entry.vecTxDSIn.size(), COINJOIN_ENTRY_MAX_SIZE, entry.vecTxOut.size(), COINJOIN_ENTRY_MAX_SIZE); + entry.vecTxDSIn.size(), nMaxEntrySize, entry.vecTxOut.size(), nMaxEntrySize); nMessageIDRet = ERR_MAXIMUM; - - CTransactionRef txCollateralToConsume; - { - LOCK(cs_coinjoin); - const auto it = std::ranges::find_if(vecSessionCollaterals, [&entry](const auto& txCollateral) { - return *entry.txCollateral == *txCollateral; - }); - if (it != vecSessionCollaterals.end()) { - txCollateralToConsume = *it; - } - } - if (txCollateralToConsume) { - ConsumeCollateral(txCollateralToConsume); - } + ConsumeCollateralIfCurrentSession(session_id, entry.txCollateral); return false; } @@ -640,6 +778,36 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag return false; } + // An entry that occupies neither side of the session denomination (empty, or a shape that + // is neither standard nor promotion/demotion) contributes nothing and must never take up + // a participant slot - it would otherwise count toward the session denomination coverage + // without providing any. + if (entry.GetMixShape() == CoinJoin::MixShape::UNKNOWN) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entry occupies no side of the session denom! inputs=%s, outputs=%s\n", + __func__, entry.vecTxDSIn.size(), entry.vecTxOut.size()); + nMessageIDRet = ERR_SIZE_MISMATCH; + ConsumeCollateralIfCurrentSession(session_id, entry.txCollateral); + return false; + } + + // Pre-V24, unbalanced entries deliberately fall through to IsValidInOuts so their + // collateral is consumed (anti-spam), same as before this feature existed + if (fRebalanceSession) { + // Every entry must match the direction its participant declared in the dsa - standard + // entries included. Admission counted on the declared shapes to decide the session + // covers both sides of the session denomination, so a participant deviating (e.g. + // declaring a promotion but submitting a standard entry) could strip a side of its + // cover and force a fee-free session reset for everyone. Deviating from the declared + // direction therefore costs the collateral. + if (entry.GetMixShape() != declaredShape) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entry shape doesn't match the declared direction! inputs=%s, outputs=%s\n", + __func__, entry.vecTxDSIn.size(), entry.vecTxOut.size()); + nMessageIDRet = ERR_SIZE_MISMATCH; + ConsumeCollateralIfCurrentSession(session_id, entry.txCollateral); + return false; + } + } + std::vector vin; for (const auto& txin : entry.vecTxDSIn) { LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- txin=%s\n", __func__, txin.ToString()); @@ -659,16 +827,30 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag } bool fConsumeCollateral{false}; - if (!IsValidInOuts(m_chainman.ActiveChainstate(), m_isman, mempool, vin, entry.vecTxOut, nMessageIDRet, - &fConsumeCollateral)) { + if (!IsValidInOuts(m_chainman.ActiveChainstate(), m_isman, mempool, vin, entry.vecTxOut, session_denom, + /*fAllowRebalanceShapes=*/fRebalanceSession, nMessageIDRet, &fConsumeCollateral)) { LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR! IsValidInOuts() failed: %s\n", __func__, CoinJoin::GetMessageByID(nMessageIDRet).translated); if (fConsumeCollateral) { - ConsumeCollateral(entry.txCollateral); + ConsumeCollateralIfCurrentSession(session_id, entry.txCollateral); } return false; } - WITH_LOCK(cs_coinjoin, vecEntries.push_back(entry)); + { + LOCK(cs_coinjoin); + // cs_coinjoin was released around the UTXO checks above, so the scheduler thread may + // have finalized or reset the session in between; re-verify so admission stays atomic + // with the state, membership and duplicate-use checks. Admitting into a session that + // already built its final transaction would add an entry no one can sign, stalling it + // until the signing timeout charges the honest participants. + if (!IsCurrentSession(session_id) || !HasSessionCollateral(entry.txCollateral) || + std::ranges::any_of(vecEntries, hasEntryForCollateral)) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: session changed while validating the entry!\n", __func__); + nMessageIDRet = ERR_SESSION; + return false; + } + vecEntries.push_back(entry); + } LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- adding entry %d of %d required\n", __func__, GetEntriesCount(), CoinJoin::GetMaxPoolParticipants()); nMessageIDRet = MSG_ENTRIES_ADDED; @@ -753,7 +935,26 @@ void CCoinJoinServer::CommitSessionCollateral(const CMutableTransaction& txColla } } -bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) +//! Which side of the session denomination a participant will occupy, from its declared dsa +//! direction. A participant that declares nothing is mixing 1:1 and occupies both sides. +static CoinJoin::MixShape DeclaredShape(const CCoinJoinAccept& dsa) +{ + if (dsa.IsPromotion()) return CoinJoin::MixShape::PROMOTION; + if (dsa.IsDemotion()) return CoinJoin::MixShape::DEMOTION; + return CoinJoin::MixShape::STANDARD; +} + +CoinJoin::MixSideCounts CCoinJoinServer::GetDeclaredSideCounts() const +{ + AssertLockHeld(cs_coinjoin); + CoinJoin::MixSideCounts counts; + for (const auto& [_, shape] : m_mapDeclaredShapes) { + counts.Add(shape); + } + return counts; +} + +bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, int nPeerVersion, PoolMessage& nMessageIDRet) { if (nSessionID != 0) return false; @@ -768,6 +969,23 @@ bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& return false; } + if (!dsa.HasValidFlags()) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateNewSession -- malformed dsa flags\n"); + nMessageIDRet = ERR_VERSION; + return false; + } + + // Post-V24: a promotion/demotion may only be declared once V24 is active and by a peer that + // speaks the rebalance protocol. The session takes its capability from what this dsa asks + // for, not from the creator's version alone - a rebalance-capable peer doing ordinary 1:1 + // mixing must not fence older clients out of the session it happens to open. + if (dsa.IsRebalance() && + (nPeerVersion < COINJOIN_REBALANCE_VERSION || !CoinJoin::IsPromotionDemotionActive(m_chainman))) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateNewSession -- rejecting rebalance dsa, promotion/demotion not active\n"); + nMessageIDRet = ERR_VERSION; + return false; + } + { LOCK(cs_coinjoin); @@ -783,6 +1001,9 @@ bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet = MSG_NOERR; nSessionID = GetRand(/*nMax=*/999999) + 1; nSessionDenom = dsa.nDenom; + m_fRebalanceSession = dsa.IsRebalance(); + m_fHasLegacyParticipant = nPeerVersion < COINJOIN_REBALANCE_VERSION; + m_mapDeclaredShapes.emplace(dsa.txCollateral.GetHash(), DeclaredShape(dsa)); SetState(POOL_STATE_QUEUE); @@ -805,35 +1026,91 @@ bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& return true; } -bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) +bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, int nPeerVersion, PoolMessage& nMessageIDRet) { - if (nSessionID == 0 || IsSessionReady()) return false; + int session_id; + int session_denom; + { + LOCK(cs_coinjoin); + if (nSessionID == 0 || nState != POOL_STATE_QUEUE) { + nMessageIDRet = ERR_MODE; + return false; + } + if (IsSessionReady()) { + nMessageIDRet = ERR_QUEUE_FULL; + return false; + } + session_id = nSessionID; + session_denom = nSessionDenom; + } if (!IsAcceptableDSA(dsa, nMessageIDRet)) { return false; } - // we only add new users to an existing session when we are in queue mode - if (nState != POOL_STATE_QUEUE) { + if (dsa.nDenom != session_denom) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- incompatible denom %d (%s) != %d (%s)\n", + dsa.nDenom, CoinJoin::DenominationToString(dsa.nDenom), session_denom, + CoinJoin::DenominationToString(session_denom)); + nMessageIDRet = ERR_DENOM; + return false; + } + + // Evaluated before taking cs_coinjoin: IsPromotionDemotionActive locks cs_main, and cs_main + // is never taken under cs_coinjoin elsewhere in this class. dsa.IsRebalance() leads so that + // cs_main is only taken for rebalance dsas. + const bool fRebalanceAcceptable = dsa.IsRebalance() && nPeerVersion >= COINJOIN_REBALANCE_VERSION && + CoinJoin::IsPromotionDemotionActive(m_chainman); + + LOCK(cs_coinjoin); + + if (nSessionID != session_id || nSessionDenom != session_denom || nState != POOL_STATE_QUEUE) { nMessageIDRet = ERR_MODE; - LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- incompatible mode: nState=%d\n", nState); + return false; + } + if (IsSessionReady()) { + nMessageIDRet = ERR_QUEUE_FULL; return false; } - if (dsa.nDenom != nSessionDenom) { - LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- incompatible denom %d (%s) != nSessionDenom %d (%s)\n", - dsa.nDenom, CoinJoin::DenominationToString(dsa.nDenom), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom)); - nMessageIDRet = ERR_DENOM; + // Checked before the rebalance gating below, which reads dsa.IsRebalance() + if (!dsa.HasValidFlags()) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- malformed dsa flags\n"); + nMessageIDRet = ERR_VERSION; return false; } - LOCK(cs_coinjoin); + // Post-V24: a promotion/demotion entry and a client that cannot validate the resulting + // unbalanced final transaction must never end up in the same session - the old client would + // refuse to sign and risk being charged its collateral. The session commits to one or the + // other on the first participant that forces the question, so a legacy 1:1 mixer is only + // turned away once a rebalance participant is actually present, not because the session + // happened to be opened by an upgraded peer. ERR_VERSION is within the message range old + // clients understand, so they reset immediately and move on to another queue. + if (dsa.IsRebalance()) { + if (!fRebalanceAcceptable) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- rejecting rebalance dsa from protocol=%d, promotion/demotion not available\n", + nPeerVersion); + nMessageIDRet = ERR_VERSION; + return false; + } + if (m_fHasLegacyParticipant) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- rejecting rebalance dsa, session holds a pre-rebalance participant\n"); + nMessageIDRet = ERR_VERSION; + return false; + } + } else if (nPeerVersion < COINJOIN_REBALANCE_VERSION && m_fRebalanceSession) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- rejecting peer with protocol=%d from rebalance session\n", + nPeerVersion); + nMessageIDRet = ERR_VERSION; + return false; + } - // A scheduler-thread timeout can reset the session via SetNull() between the checks above - // and taking cs_coinjoin, so revalidate: a collateral must never be committed to a session - // that no longer exists. - if (nSessionID == 0 || nState != POOL_STATE_QUEUE) { - nMessageIDRet = ERR_MODE; + // IsSessionReady() can now hold a full session back waiting for a missing counterparty, so + // the participant limit has to be enforced here rather than implied by session readiness + if (static_cast(vecSessionCollaterals.size()) >= CoinJoin::GetMaxPoolParticipants()) { + LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- session is full\n"); + nMessageIDRet = ERR_QUEUE_FULL; return false; } @@ -852,6 +1129,11 @@ bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolM // count new user as accepted to an existing session nMessageIDRet = MSG_NOERR; + // Latch what this participant commits the session to. The checks above guarantee the two + // never both become true. + m_fRebalanceSession |= dsa.IsRebalance(); + m_fHasLegacyParticipant |= nPeerVersion < COINJOIN_REBALANCE_VERSION; + m_mapDeclaredShapes.emplace(dsa.txCollateral.GetHash(), DeclaredShape(dsa)); CommitSessionCollateral(dsa.txCollateral); LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- new user accepted, nSessionID: %d nSessionDenom: %d (%s) vecSessionCollaterals.size(): %d CoinJoin::GetMaxPoolParticipants(): %d\n", @@ -864,10 +1146,14 @@ bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolM bool CCoinJoinServer::IsSessionReady() const { if (nState == POOL_STATE_QUEUE) { - if ((int)vecSessionCollaterals.size() >= CoinJoin::GetMaxPoolParticipants()) { + // PRIVACY: don't start mixing until each side of the session denomination is occupied + // by nobody or by at least two participants. A session that never attracts the missing + // counterparty simply expires in queue state, where no collateral is charged. + if (!GetDeclaredSideCounts().IsCovered()) return false; + if (static_cast(vecSessionCollaterals.size()) >= CoinJoin::GetMaxPoolParticipants()) { return true; } - if (CCoinJoinServer::HasTimedOut() && (int)vecSessionCollaterals.size() >= CoinJoin::GetMinPoolParticipants()) { + if (CCoinJoinServer::HasTimedOut() && static_cast(vecSessionCollaterals.size()) >= CoinJoin::GetMinPoolParticipants()) { return true; } } diff --git a/src/coinjoin/server.h b/src/coinjoin/server.h index 0c11576118fd..080aae76716f 100644 --- a/src/coinjoin/server.h +++ b/src/coinjoin/server.h @@ -12,6 +12,7 @@ #include #include +#include #include class CActiveMasternodeManager; @@ -43,6 +44,10 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler const CMasternodeSync& m_mn_sync; const llmq::CInstantSendManager& m_isman; +protected: + // Session state and entry admission live in the protected section so unit tests can seed + // and drive them through a test subclass. + // Mixing uses collateral transactions to trust parties entering the pool // to behave honestly. If they don't it takes their money. std::vector vecSessionCollaterals; @@ -50,34 +55,75 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler // reuses one of them can be rejected without rescanning them all. std::unordered_set setSessionCollateralPrevouts GUARDED_BY(cs_coinjoin); - bool fUnitTest; + // Post-V24: true once a participant has been admitted that declared a promotion/demotion, + // i.e. the final transaction may come out unbalanced. Latched on admission rather than + // fixed by the creator's protocol version, so a session only commits to this once someone + // actually asks for it. + bool m_fRebalanceSession GUARDED_BY(cs_coinjoin){false}; + // Post-V24: true once a participant below COINJOIN_REBALANCE_VERSION has been admitted. + // Such a peer cannot validate an unbalanced final transaction, so it must never share a + // session with a rebalance participant. Mutually exclusive with m_fRebalanceSession. + bool m_fHasLegacyParticipant GUARDED_BY(cs_coinjoin){false}; + // The mixing direction each accepted participant declared in its dsa, keyed by collateral + // hash. Tells us which side of the session denomination a participant will occupy before + // its entry arrives, and entitles it (and only it) to submit an entry of that shape. + std::map m_mapDeclaredShapes GUARDED_BY(cs_coinjoin); + + /// Sides of the session denomination the accepted participants declared they will occupy + CoinJoin::MixSideCounts GetDeclaredSideCounts() const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); /// Add a clients entry to the pool bool AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + /// Record an accepted collateral and index its input prevouts + void CommitSessionCollateral(const CMutableTransaction& txCollateral) EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); + +private: + bool fUnitTest; + /// Add signature to a txin bool AddScriptSig(const CTxIn& txin) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); /// Charge fees to bad actors (Charge clients a fee if they're abusive) void ChargeFees() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); /// Rarely charge fees to pay miners - void ChargeRandomFees() const; - /// Consume collateral in cases when peer misbehaved - void ConsumeCollateral(const CTransactionRef& txref) const; + void ChargeRandomFees() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + /// Consume collateral in cases when peer misbehaved. Takes cs_main, which this class never + /// takes under cs_coinjoin. + void ConsumeCollateral(const CTransactionRef& txref) const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + /// Consume collateral, but only while session_id is still the live session holding it + void ConsumeCollateralIfCurrentSession(int session_id, const CTransactionRef& txref) const + EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + + /// Is session_id still the session we are accepting entries for? + bool IsCurrentSession(int session_id) const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); + + //! One consistent view of the session, for decisions that read several pieces of its state. + //! Sampling them one at a time lets the message-handling thread commit an entry in between, + //! producing a mix of old and new values that describes no state the session was ever in. + struct PoolSnapshot { + int session_id{0}; + PoolState state{POOL_STATE_IDLE}; + size_t entries{0}; + size_t collaterals{0}; + CoinJoin::MixSideCounts sides; + }; + PoolSnapshot GetPoolSnapshot() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + /// Is txref one of the collaterals accepted into the current session? + bool HasSessionCollateral(const CTransactionRef& txref) const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); /// Check for process void CheckPool(); - void CreateFinalTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + /// Build and relay the final transaction, unless session_id is no longer the live session + void CreateFinalTransaction(int session_id) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); void CommitFinalTransaction() EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); /// Is this nDenom and txCollateral acceptable? bool IsAcceptableDSA(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) const; - /// Record an accepted collateral and index its input prevouts - void CommitSessionCollateral(const CMutableTransaction& txCollateral) EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); - bool CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); - bool AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + bool CreateNewSession(const CCoinJoinAccept& dsa, int nPeerVersion, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); + bool AddUserToExistingSession(const CCoinJoinAccept& dsa, int nPeerVersion, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); /// Do we have enough users to take entries? - bool IsSessionReady() const; + bool IsSessionReady() const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); /// Check that all inputs are signed. (Are all inputs signed?) bool IsSignaturesComplete() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); diff --git a/src/coinjoin/util.cpp b/src/coinjoin/util.cpp index e24ae8b31fac..807ff46c5d3e 100644 --- a/src/coinjoin/util.cpp +++ b/src/coinjoin/util.cpp @@ -262,7 +262,7 @@ int CTransactionBuilder::GetSizeOfCompactSizeDiff(size_t nAdd) const size_t nSize = WITH_LOCK(cs_outputs, return vecOutputs.size()); unsigned int ret = ::GetSizeOfCompactSizeDiff(nSize, nSize + nAdd); assert(ret <= std::numeric_limits::max()); - return int(ret); + return static_cast(ret); } bool CTransactionBuilder::IsDust(CAmount nAmount) const diff --git a/src/core_io.h b/src/core_io.h index 85deffd5c2fd..6785fab15a15 100644 --- a/src/core_io.h +++ b/src/core_io.h @@ -7,6 +7,7 @@ #include +#include #include #include @@ -18,7 +19,7 @@ class CTxUndo; class uint256; struct CMutableTransaction; struct CSpentIndexTxInfo; -struct RPCResult; +enum class MnType : uint16_t; class UniValue; @@ -58,6 +59,11 @@ void ScriptToUniv(const CScript& script, UniValue& out, bool include_hex = true, void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry, bool include_hex = true, int serialize_flags = 0, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS, const CSpentIndexTxInfo* ptxSpentInfo = nullptr); // evo/core_write.cpp -RPCResult GetRpcResult(const std::string& key, bool optional = false, const std::string& override_name = ""); +/** Reads network info reporting and appends data from legacy fields if applicable */ +template +UniValue GetNetInfoWithLegacyFields(const Obj& obj, const MnType& type); +/** Returns platform port based on purpose and network info version */ +template +int32_t GetPlatformPort(const Obj& obj); #endif // BITCOIN_CORE_IO_H diff --git a/src/evo/assetlocktx.h b/src/evo/assetlocktx.h index 3dea2c5e13a8..9908f821f201 100644 --- a/src/evo/assetlocktx.h +++ b/src/evo/assetlocktx.h @@ -36,6 +36,17 @@ class CAssetLockPayload static constexpr uint8_t CURRENT_VERSION = 2; static constexpr auto SPECIALTX_TYPE = TRANSACTION_ASSET_LOCK; + //! Highest payload version CheckAssetLockTx accepts under the given + //! deployment state, and therefore the version to use for new asset + //! locks. Callers building a transaction must pass whether v24 applies + //! to the *next* block (the state it will be validated against); version + //! 1 remains valid after activation, so a transaction built right before + //! the boundary and mined after it stays valid. + static constexpr uint8_t GetMaxVersion(bool is_v24_active) + { + return is_v24_active ? CURRENT_VERSION : INITIAL_VERSION; + } + private: uint8_t nVersion{CURRENT_VERSION}; std::vector creditOutputs; diff --git a/src/evo/chainhelper.cpp b/src/evo/chainhelper.cpp index acffa3cd2d4a..9412607e93a0 100644 --- a/src/evo/chainhelper.cpp +++ b/src/evo/chainhelper.cpp @@ -7,14 +7,17 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include #include +#include CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmnman, const CMasternodeSync& mn_sync, llmq::CInstantSendManager& isman, llmq::CQuorumBlockProcessor& qblockman, @@ -23,6 +26,7 @@ CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmn const llmq::CQuorumManager& qman) : isman{isman}, mn_sync{mn_sync}, + m_dmnman{dmnman}, credit_pool_manager{std::make_unique(evodb, chainman)}, m_chainlocks{chainlocks}, ehf_manager{std::make_unique(evodb, chainman)}, @@ -60,6 +64,11 @@ bool CChainstateHelper::HasChainLock(int nHeight, const uint256& blockHash) cons int32_t CChainstateHelper::GetBestChainLockHeight() const { return m_chainlocks.GetBestChainLockHeight(); } +uint256 CChainstateHelper::GetDeterministicMNListHash(const CBlockIndex* pindex) const +{ + return SerializeHash(m_dmnman.GetListForBlock(Assert(pindex))); +} + /** Passthrough functions to CCreditPoolManager */ CCreditPool CChainstateHelper::GetCreditPool(const CBlockIndex* const pindex) { diff --git a/src/evo/chainhelper.h b/src/evo/chainhelper.h index f68c48bd26bf..eac183777ba1 100644 --- a/src/evo/chainhelper.h +++ b/src/evo/chainhelper.h @@ -42,6 +42,7 @@ class CChainstateHelper private: llmq::CInstantSendManager& isman; const CMasternodeSync& mn_sync; + CDeterministicMNManager& m_dmnman; public: const std::unique_ptr credit_pool_manager; @@ -69,6 +70,9 @@ class CChainstateHelper bool HasChainLock(int nHeight, const uint256& blockHash) const; int32_t GetBestChainLockHeight() const; + /** Return a canonical hash of the deterministic MN list derived at a block. */ + uint256 GetDeterministicMNListHash(const CBlockIndex* pindex) const; + /** Passthrough functions to CCreditPoolManager */ CCreditPool GetCreditPool(const CBlockIndex* const pindex); diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 7ebfd295e457..4ebe78675f2d 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include #include @@ -12,80 +12,21 @@ #include #include #include -#include #include #include #include -#include +#include #include