diff --git a/Cargo.lock b/Cargo.lock index e315b28c3..8386ff04d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.65" @@ -964,6 +973,7 @@ dependencies = [ "memuse", "metrics", "metrics-exporter-prometheus", + "metrics-util", "nonempty", "orchard", "rand 0.8.5", @@ -977,6 +987,7 @@ dependencies = [ "subtle", "thiserror", "time", + "tokio", "tracing", "tracing-appender", "tracing-core", @@ -1108,6 +1119,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d24dc2dbae22bff6f1f9326ffce828c9f07ef9cc1e8002e5279f845432a30a" dependencies = [ + "aho-corasick", "crossbeam-epoch", "crossbeam-utils", "hashbrown", diff --git a/Cargo.toml b/Cargo.toml index 1dd38984d..fec8a8a11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,8 @@ rayon = "1.5" ipnet = "2" metrics = "0.20" metrics-exporter-prometheus = "0.11" +metrics-util = { version = "0.14", default-features = false, features = ["layer-filter"] } +tokio = { version = "1", features = ["rt", "net", "time"] } # General tool dependencies gumdrop = "0.8" diff --git a/README.md b/README.md index 9d9a121a5..df635786a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Zcash 5.3.0 +Zcash 5.3.2 =========== diff --git a/configure.ac b/configure.ac index 4421ed480..5510e89c1 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 5) define(_CLIENT_VERSION_MINOR, 3) -define(_CLIENT_VERSION_REVISION, 0) +define(_CLIENT_VERSION_REVISION, 2) define(_CLIENT_VERSION_BUILD, 50) define(_ZC_BUILD_VAL, m4_if(m4_eval(_CLIENT_VERSION_BUILD < 25), 1, m4_incr(_CLIENT_VERSION_BUILD), m4_eval(_CLIENT_VERSION_BUILD < 50), 1, m4_eval(_CLIENT_VERSION_BUILD - 24), m4_eval(_CLIENT_VERSION_BUILD == 50), 1, , m4_eval(_CLIENT_VERSION_BUILD - 50))) define(_CLIENT_VERSION_SUFFIX, m4_if(m4_eval(_CLIENT_VERSION_BUILD < 25), 1, _CLIENT_VERSION_REVISION-beta$1, m4_eval(_CLIENT_VERSION_BUILD < 50), 1, _CLIENT_VERSION_REVISION-rc$1, m4_eval(_CLIENT_VERSION_BUILD == 50), 1, _CLIENT_VERSION_REVISION, _CLIENT_VERSION_REVISION-$1))) diff --git a/contrib/debian/changelog b/contrib/debian/changelog index 2afc8e388..18a5d09db 100644 --- a/contrib/debian/changelog +++ b/contrib/debian/changelog @@ -1,3 +1,21 @@ +zcash (5.3.2) stable; urgency=medium + + * 5.3.2 release. + + -- Electric Coin Company Sat, 03 Dec 2022 19:58:44 +0000 + +zcash (5.3.1) stable; urgency=medium + + * 5.3.1 release. + + -- Electric Coin Company Fri, 02 Dec 2022 02:46:42 +0000 + +zcash (5.3.1~rc1) stable; urgency=medium + + * 5.3.1-rc1 release. + + -- Electric Coin Company Wed, 23 Nov 2022 22:18:55 -0700 + zcash (5.3.0) stable; urgency=medium * 5.3.0 release. diff --git a/contrib/docker/entrypoint.sh b/contrib/docker/entrypoint.sh index bb1d3a7f6..44e3f22d1 100755 --- a/contrib/docker/entrypoint.sh +++ b/contrib/docker/entrypoint.sh @@ -55,4 +55,5 @@ fi zcash-fetch-params touch .zcash/zcash.conf echo "Starting: ${ZCASHD_CMD}" +# shellcheck disable=SC2294 # see #6297 eval exec "${ZCASHD_CMD}" "${@}" diff --git a/contrib/gitian-descriptors/gitian-linux-parallel.yml b/contrib/gitian-descriptors/gitian-linux-parallel.yml index dda65b5f4..05958bb2d 100644 --- a/contrib/gitian-descriptors/gitian-linux-parallel.yml +++ b/contrib/gitian-descriptors/gitian-linux-parallel.yml @@ -1,5 +1,5 @@ --- -name: "zcash-5.3.0" +name: "zcash-5.3.2" enable_cache: true distro: "debian" suites: diff --git a/contrib/gitian-descriptors/gitian-linux.yml b/contrib/gitian-descriptors/gitian-linux.yml index adb5c5a1d..eac535402 100644 --- a/contrib/gitian-descriptors/gitian-linux.yml +++ b/contrib/gitian-descriptors/gitian-linux.yml @@ -1,5 +1,5 @@ --- -name: "zcash-5.3.0" +name: "zcash-5.3.2" enable_cache: true distro: "debian" suites: diff --git a/depends/packages/native_clang.mk b/depends/packages/native_clang.mk index 37a3c2c31..c431bf520 100644 --- a/depends/packages/native_clang.mk +++ b/depends/packages/native_clang.mk @@ -20,6 +20,7 @@ $(package)_download_path_freebsd=https://github.com/llvm/llvm-project/releases/d $(package)_download_file_freebsd=clang+llvm-$($(package)_version)-amd64-unknown-freebsd12.tar.xz $(package)_file_name_freebsd=clang-llvm-$($(package)_version)-amd64-unknown-freebsd12.tar.xz $(package)_sha256_hash_freebsd=b0a7b86dacb12afb8dd2ca99ea1b894d9cce84aab7711cb1964b3005dfb09af3 +$(package)_download_path_aarch64_linux=https://github.com/llvm/llvm-project/releases/download/llvmorg-$($(package)_version) $(package)_download_file_aarch64_linux=clang+llvm-$($(package)_version)-aarch64-linux-gnu.tar.xz $(package)_file_name_aarch64_linux=clang-llvm-$($(package)_version)-aarch64-linux-gnu.tar.xz $(package)_sha256_hash_aarch64_linux=1a81fda984f5e607584916fdf69cf41e5385b219b983544d2c1a14950d5a65cf diff --git a/depends/packages/utfcpp.mk b/depends/packages/utfcpp.mk index 2a0620e21..85b99402b 100644 --- a/depends/packages/utfcpp.mk +++ b/depends/packages/utfcpp.mk @@ -6,5 +6,6 @@ $(package)_download_file=v$($(package)_version).tar.gz $(package)_sha256_hash=8d6aa7d77ad0abb35bb6139cb9a33597ac4c5b33da6a004ae42429b8598c9605 define $(package)_stage_cmds - cp -a ./source $($(package)_staging_dir)$(host_prefix)/include + mkdir -p $($(package)_staging_dir)$(host_prefix)/include && \ + cp -a ./source $($(package)_staging_dir)$(host_prefix)/include/utf8cpp endef diff --git a/doc/authors.md b/doc/authors.md index d3a1e5457..949730a62 100644 --- a/doc/authors.md +++ b/doc/authors.md @@ -1,11 +1,11 @@ Zcash Contributors ================== -Jack Grigg (1243) -Kris Nuttycombe (582) +Jack Grigg (1244) +Kris Nuttycombe (585) Simon Liu (460) Sean Bowe (389) -Daira Hopwood (343) +Daira Hopwood (369) Eirik Ogilvie-Wigley (216) Wladimir J. van der Laan (158) Pieter Wuille (143) @@ -18,8 +18,9 @@ Jay Graber (89) Larry Ruane (88) Marco Falke (86) Cory Fields (78) +sasha (62) Matt Corallo (60) -sasha (59) +Greg Pfeil (60) Nathan Wilcox (57) practicalswift (42) Kevin Gallagher (38) @@ -30,7 +31,6 @@ Carl Dong (26) Gregory Maxwell (24) John Newbery (23) Jorge Timón (22) -Greg Pfeil (20) furszy (18) Jonathan "Duke" Leto (18) syd (16) @@ -38,13 +38,13 @@ Patick Strateman (16) Charlie O'Keefe (15) avnish (14) Per Grön (14) +Suhas Daftuar (13) Benjamin Winston (13) Steven Smith (12) Pavel Janík (12) Patrick Strateman (12) Jeremy Rubin (12) Ariel Gabizon (12) -Suhas Daftuar (11) Paige Peterson (11) Kaz Wesley (11) João Barbosa (11) @@ -123,6 +123,7 @@ Pejvan (2) Pavol Rusnak (2) Pavel Vasin (2) Mustafa (2) +Miodrag Popović (2) Matthew King (2) Mary Moore-Simmons (2) Marek (2) @@ -159,6 +160,7 @@ kirkalx (1) kazcw (1) jeff-liang (1) jc (1) +idm (1) glowang (1) ewillbefull@gmail.com (1) emilrus (1) diff --git a/doc/man/zcash-cli.1 b/doc/man/zcash-cli.1 index 6bc31fbcb..b0157bf5d 100644 --- a/doc/man/zcash-cli.1 +++ b/doc/man/zcash-cli.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH ZCASH-CLI "1" "October 2022" "zcash-cli v5.3.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.2. +.TH ZCASH-CLI "1" "December 2022" "zcash-cli v5.3.2" "User Commands" .SH NAME -zcash-cli \- manual page for zcash-cli v5.3.0 +zcash-cli \- manual page for zcash-cli v5.3.2 .SH DESCRIPTION -Zcash RPC client version v5.3.0 +Zcash RPC client version v5.3.2 .PP In order to ensure you are adequately protecting your privacy when using Zcash, please see . diff --git a/doc/man/zcash-tx.1 b/doc/man/zcash-tx.1 index ce8c1ae64..b39e05caa 100644 --- a/doc/man/zcash-tx.1 +++ b/doc/man/zcash-tx.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH ZCASH-TX "1" "October 2022" "zcash-tx v5.3.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.2. +.TH ZCASH-TX "1" "December 2022" "zcash-tx v5.3.2" "User Commands" .SH NAME -zcash-tx \- manual page for zcash-tx v5.3.0 +zcash-tx \- manual page for zcash-tx v5.3.2 .SH DESCRIPTION -Zcash zcash\-tx utility version v5.3.0 +Zcash zcash\-tx utility version v5.3.2 .SS "Usage:" .TP zcash\-tx [options] [commands] diff --git a/doc/man/zcashd-wallet-tool.1 b/doc/man/zcashd-wallet-tool.1 index 2c19ea2c0..3a2fc540f 100644 --- a/doc/man/zcashd-wallet-tool.1 +++ b/doc/man/zcashd-wallet-tool.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH ZCASHD-WALLET-TOOL "1" "October 2022" "zcashd-wallet-tool v5.3.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.2. +.TH ZCASHD-WALLET-TOOL "1" "December 2022" "zcashd-wallet-tool v5.3.2" "User Commands" .SH NAME -zcashd-wallet-tool \- manual page for zcashd-wallet-tool v5.3.0 +zcashd-wallet-tool \- manual page for zcashd-wallet-tool v5.3.2 .SH SYNOPSIS .B zcashd-wallet-tool [\fI\,OPTIONS\/\fR] diff --git a/doc/man/zcashd.1 b/doc/man/zcashd.1 index 090c5a511..e59a2a012 100644 --- a/doc/man/zcashd.1 +++ b/doc/man/zcashd.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH ZCASHD "1" "October 2022" "zcashd v5.3.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.2. +.TH ZCASHD "1" "December 2022" "zcashd v5.3.2" "User Commands" .SH NAME -zcashd \- manual page for zcashd v5.3.0 +zcashd \- manual page for zcashd v5.3.2 .SH DESCRIPTION -Zcash Daemon version v5.3.0 +Zcash Daemon version v5.3.2 .PP In order to ensure you are adequately protecting your privacy when using Zcash, please see . @@ -94,7 +94,7 @@ Keep at most unconnectable transactions in memory (default: 100) .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-8\fR to 16, 0 = auto, <0 = +Set the number of script verification threads (\fB\-6\fR to 16, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-pid=\fR @@ -416,6 +416,10 @@ Expose node metrics in the Prometheus exposition format. An HTTP listener will be started on , which responds to GET requests on any request path. Use \fB\-metricsallowip\fR and \fB\-metricsbind\fR to control access. +.HP +\fB\-debugmetrics\fR +.IP +Include debug metrics in exposed node metrics. .PP Debugging/Testing options: .HP diff --git a/doc/release-notes.md b/doc/release-notes.md index 1a3db57d8..a29094b51 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -4,8 +4,3 @@ release-notes at release time) Notable changes =============== -Fixed ------ - -This release fixes an error "Assertion `uResultHeight == rewindHeight` failed" (#5958) -that could sometimes happen when restarting a node. diff --git a/doc/release-notes/release-notes-5.3.1-rc1.md b/doc/release-notes/release-notes-5.3.1-rc1.md new file mode 100644 index 000000000..fe841d4dc --- /dev/null +++ b/doc/release-notes/release-notes-5.3.1-rc1.md @@ -0,0 +1,99 @@ +Notable changes +=============== + +Fixed +----- + +This release fixes an error "Assertion `uResultHeight == rewindHeight` failed" (#5958) +that could sometimes happen when restarting a node. + +Memory Usage Improvement +------------------------ + +The memory usage of zcashd has been reduced by not keeping Equihash solutions for all +block headers in memory. + +Changelog +========= + +Daira Hopwood (14): + Always use a tuple as right argument of % in new Python code. + Report the prevout for each transparent input as it is being checked + Update authors of librustzcash to include Greg Pfeil. + Ensure that the optimization of not scanning blocks prior to the wallet's birthday does not cause us to try to "rewind" the Orchard wallet to a height after its current checkpoint. + Improve a comment about the wallet birthday scanning optimization. + Add release notes for the fix to #5958. + Fix a Markdown syntax error + Error reporting improvements. + Fix a dependency of the `show_help` RPC test on the number of cores, and an incompatibility with Python 3.9 in the test framework that affected the `receivedby` extended RPC test. + Avoid storing the Equihash solution in a CBlockIndex object once it has been written to the leveldb database. + Improve handling of database read errors. + Add Prometheus metrics so we have more visibility into what is going on with the Equihash solution trimming: + Declare `CBlockTreeDB::Read*` methods as `const` when they are trivially so. + Update constants + +Greg Pfeil (39): + Fix display of binary name in error messages. + Address review feedback and fixed test failures + Check dependency updates on the correct branch + updatecheck: fix GitHub auth + updatecheck: simplify token handling + updatecheck: support XDG-based token location + `zcash --help` test improvements + Remove the PR template + Apply suggestions from code review + Small formatting change + Improve z_sendmany documentation + Avoid inconsistent Python lookup + Propagate asOfHeight to all relevant RPC calls + Implement `asOfHeight` + Add additional asOfHeight tests + Don’t ignore asOfHeight in IsSpent calls + Extract asOfHeight info from RPC calls + Ignore mempool when asOfHeight is set + Fix calls that should have specified asOfHeight + GetUnconfirmedBalance should not take asOfHeight + Require minconf > 0 when asOfHeight is provided + Add error cases and default to `asOfHeight` + Work around #6262 in wallet_listunspent + Don’t trust mempool tx when using `asOfHeight` + Apply suggestions from code review + Add matured_at_height test helper + Add FIXMEs to repair comments after #6262 is fixed + Update src/rpc/server.cpp + Apply suggestions from code review + Fix small error in code review suggestions + Revert change to getbalance minconf + Revert getinfo support of asOfHeight + Change asOfHeight to use -1 as default + Change asOfHeight to preserve Bitcoin compat + Apply suggestions from code review + Simplify filtering AvailableCoins by destination + Postpone dependency updates for v5.3.1 + make-release.py: Versioning changes for 5.3.1-rc1. + make-release.py: Updated manpages for 5.3.1-rc1. + +Jack Grigg (1): + Place zcashd.debug.* metrics behind a -debugmetrics config option + +Kris Nuttycombe (3): + Add extra detail related to transparent inputs and outputs. + Add `unspent_as_of` argument to `listunspent` + Add RPC test for wallet_listunspent changes + +Miodrag Popović (2): + FindNextBlocksToDownload(): Fetch active consensus params to read nMinimumChainWork + Headers sync timeout: Use EstimateNetHeight() for closer approximation of remaining headers to download + +Suhas Daftuar (2): + Delay parallel block download until chain has sufficient work + Add timeout for headers sync + +idm (1): + fix aarch64 dependency native clang download URL + +sasha (3): + Update gitian-linux-parallel.yml + Fix gitian version string issue by reverting the GIT_DIR backport commit + Remove `git_check_in_repo` from genbuild.sh to fix gitian version string + diff --git a/doc/release-notes/release-notes-5.3.1.md b/doc/release-notes/release-notes-5.3.1.md new file mode 100644 index 000000000..0b1c23103 --- /dev/null +++ b/doc/release-notes/release-notes-5.3.1.md @@ -0,0 +1,135 @@ +Notable changes +=============== + +Fixed +----- + +This release fixes an error "Assertion `uResultHeight == rewindHeight` failed" (#5958) +that could sometimes happen when restarting a node. + +Memory Usage Improvement +------------------------ + +The memory usage of zcashd has been reduced by not keeping Equihash solutions for all +block headers in memory. + +RPC changes +----------- + +The following RPC methods that query wallet state now support an optional `asOfHeight` +parameter, to execute the query as if it were run when the blockchain was at the height +specified by this argument: + +* `getbalance` +* `getreceivedbyaddress` +* `gettransaction` (*) +* `getwalletinfo` +* `listaddressgroupings` +* `listreceivedbyaddress` (*) +* `listsinceblock` (*) +* `listtransactions` +* `listunspent` (*) +* `z_getbalanceforaccount` +* `z_getbalanceforviewingkey` +* `z_getmigrationstatus` +* `z_getnotescount` +* `z_listreceivedbyaddress` +* `z_listunspent` + +(*) For these methods, additional parameters have been added to maintain +compatibility of parameter lists with Bitcoin Core. Default values should be +passed for these additional parameters in order to use `asOfHeight`. See the +[RPC documentation](https://zcash.github.io/) for details. + +Changelog +========= + +Daira Hopwood (21): + Always use a tuple as right argument of % in new Python code. + Report the prevout for each transparent input as it is being checked + Update authors of librustzcash to include Greg Pfeil. + Ensure that the optimization of not scanning blocks prior to the wallet's birthday does not cause us to try to "rewind" the Orchard wallet to a height after its current checkpoint. + Improve a comment about the wallet birthday scanning optimization. + Add release notes for the fix to #5958. + Fix a Markdown syntax error + Error reporting improvements. + Fix a dependency of the `show_help` RPC test on the number of cores, and an incompatibility with Python 3.9 in the test framework that affected the `receivedby` extended RPC test. + Avoid storing the Equihash solution in a CBlockIndex object once it has been written to the leveldb database. + Improve handling of database read errors. + Add Prometheus metrics so we have more visibility into what is going on with the Equihash solution trimming: + Declare `CBlockTreeDB::Read*` methods as `const` when they are trivially so. + Update constants + Add release notes for #6122 (`asOfHeight` RPC parameters). + Update release notes: z_getnotescount already had the minconf parameter + Add release notes for #6122 (`asOfHeight` RPC parameters). + Update release notes: z_getnotescount already had the minconf parameter + Bump timestamps and add libcxx/native_clang 15.0.6 in `postponed-updates.txt`. + make-release.py: Versioning changes for 5.3.1. + make-release.py: Updated manpages for 5.3.1. + +Greg Pfeil (40): + Fix display of binary name in error messages. + Address review feedback and fixed test failures + Check dependency updates on the correct branch + updatecheck: fix GitHub auth + updatecheck: simplify token handling + updatecheck: support XDG-based token location + `zcash --help` test improvements + Remove the PR template + Apply suggestions from code review + Small formatting change + Improve z_sendmany documentation + Avoid inconsistent Python lookup + Propagate asOfHeight to all relevant RPC calls + Implement `asOfHeight` + Add additional asOfHeight tests + Don’t ignore asOfHeight in IsSpent calls + Extract asOfHeight info from RPC calls + Ignore mempool when asOfHeight is set + Fix calls that should have specified asOfHeight + GetUnconfirmedBalance should not take asOfHeight + Require minconf > 0 when asOfHeight is provided + Add error cases and default to `asOfHeight` + Work around #6262 in wallet_listunspent + Don’t trust mempool tx when using `asOfHeight` + Apply suggestions from code review + Add matured_at_height test helper + Add FIXMEs to repair comments after #6262 is fixed + Update src/rpc/server.cpp + Apply suggestions from code review + Fix small error in code review suggestions + Revert change to getbalance minconf + Revert getinfo support of asOfHeight + Change asOfHeight to use -1 as default + Change asOfHeight to preserve Bitcoin compat + Apply suggestions from code review + Simplify filtering AvailableCoins by destination + Postpone dependency updates for v5.3.1 + make-release.py: Versioning changes for 5.3.1-rc1. + make-release.py: Updated manpages for 5.3.1-rc1. + make-release.py: Updated release notes and changelog for 5.3.1-rc1. + +Jack Grigg (1): + Place zcashd.debug.* metrics behind a -debugmetrics config option + +Kris Nuttycombe (3): + Add extra detail related to transparent inputs and outputs. + Add `unspent_as_of` argument to `listunspent` + Add RPC test for wallet_listunspent changes + +Miodrag Popović (2): + FindNextBlocksToDownload(): Fetch active consensus params to read nMinimumChainWork + Headers sync timeout: Use EstimateNetHeight() for closer approximation of remaining headers to download + +Suhas Daftuar (2): + Delay parallel block download until chain has sufficient work + Add timeout for headers sync + +idm (1): + fix aarch64 dependency native clang download URL + +sasha (3): + Update gitian-linux-parallel.yml + Fix gitian version string issue by reverting the GIT_DIR backport commit + Remove `git_check_in_repo` from genbuild.sh to fix gitian version string + diff --git a/doc/release-notes/release-notes-5.3.2.md b/doc/release-notes/release-notes-5.3.2.md new file mode 100644 index 000000000..73d736490 --- /dev/null +++ b/doc/release-notes/release-notes-5.3.2.md @@ -0,0 +1,26 @@ +Notable changes +=============== + +Fixed +----- + +This is a hotfix release that fixes a regression in memory usage during +Initial Block Download. The regression was indirectly caused by a change +to prioritize downloading headers (PR #6231), introduced in release 5.3.1. +It caused memory usage for new nodes to spike to roughly 11 GiB about an +hour after starting Initial Block Download. + +The issue fixed by this release does not affect nodes that start from +a fully synced chain, or that had sufficient memory available to get +past the memory usage spike. + +Changelog +========= + +Daira Hopwood (5): + Revert "Headers-first fix" + Add release notes for the IBD memory spike issue. + Postpone updates. + make-release.py: Versioning changes for 5.3.2. + make-release.py: Updated manpages for 5.3.2. + diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 33755dca4..0b8327377 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -62,6 +62,7 @@ BASE_SCRIPTS= [ 'wallet_overwintertx.py', 'wallet_persistence.py', 'wallet_listnotes.py', + 'wallet_listunspent.py', # vv Tests less than 60s vv 'orchard_reorg.py', 'fundrawtransaction.py', diff --git a/qa/rpc-tests/listtransactions.py b/qa/rpc-tests/listtransactions.py index 979098c4f..bdf7f622c 100755 --- a/qa/rpc-tests/listtransactions.py +++ b/qa/rpc-tests/listtransactions.py @@ -7,9 +7,17 @@ # Exercise the listtransactions API from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal from decimal import Decimal +def count_array_matches(object_array, to_match): + num_matched = 0 + for item in object_array: + if all((item[key] == value for key,value in to_match.items())): + num_matched = num_matched+1 + return num_matched + def check_array_result(object_array, to_match, expected): """ Pass in array of JSON objects, a dictionary with key/value pairs @@ -27,7 +35,7 @@ def check_array_result(object_array, to_match, expected): for key,value in expected.items(): if item[key] != value: raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value))) - num_matched = num_matched+1 + num_matched = num_matched+1 if num_matched == 0: raise AssertionError("No objects matched %s"%(str(to_match))) @@ -45,6 +53,7 @@ class ListTransactionsTest(BitcoinTestFramework): {"category":"receive","amount":Decimal("0.1"),"amountZat":10000000,"confirmations":0}) # mine a block, confirmations should change: + old_block = self.nodes[0].getblockcount() self.nodes[0].generate(1) self.sync_all() check_array_result(self.nodes[0].listtransactions(), @@ -53,6 +62,16 @@ class ListTransactionsTest(BitcoinTestFramework): check_array_result(self.nodes[1].listtransactions(), {"txid":txid}, {"category":"receive","amount":Decimal("0.1"),"amountZat":10000000,"confirmations":1}) + # Confirmations here are -1 instead of 0 because while we only went back + # 1 block, “0” means the tx is in the mempool, but the tx has already + # been mined and `asOfHeight` ignores the mempool regardless. + check_array_result(self.nodes[0].listtransactions("*", 10, 0, False, old_block), + {"txid":txid}, + {"category":"send","amount":Decimal("-0.1"),"amountZat":-10000000,"confirmations":-1}) + # And while we can still see the tx we sent before the block where they + # got mined, we don’t have access to ones we’ll receive after the + # specified block. + assert_equal(0, count_array_matches(self.nodes[1].listtransactions("*", 10, 0, False, old_block), {"txid":txid})) # send-to-self: txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 0.2) diff --git a/qa/rpc-tests/receivedby.py b/qa/rpc-tests/receivedby.py index c91cc56ea..85ca57680 100755 --- a/qa/rpc-tests/receivedby.py +++ b/qa/rpc-tests/receivedby.py @@ -14,7 +14,7 @@ from decimal import Decimal def get_sub_array_from_array(object_array, to_match): ''' Finds and returns a sub array from an array of arrays. - to_match should be a unique idetifier of a sub array + to_match should be a unique identifier of a sub array ''' for item in object_array: all_match = True diff --git a/qa/rpc-tests/show_help.py b/qa/rpc-tests/show_help.py index eb0d473d5..e2f7bd773 100755 --- a/qa/rpc-tests/show_help.py +++ b/qa/rpc-tests/show_help.py @@ -13,7 +13,7 @@ from test_framework.util import assert_equal, assert_true, zcashd_binary import subprocess import tempfile -help_message = """ +help_message_1 = """ In order to ensure you are adequately protecting your privacy when using Zcash, please see . @@ -85,7 +85,8 @@ Options: Keep at most unconnectable transactions in memory (default: 100) -par= - Set the number of script verification threads (-8 to 16, 0 = auto, <0 = + Set the number of script verification threads (""" # nondeterministic part here +help_message_2 = """, 0 = auto, <0 = leave that many cores free, default: 0) -pid= @@ -342,6 +343,9 @@ Monitoring options: any request path. Use -metricsallowip and -metricsbind to control access. + -debugmetrics + Include debug metrics in exposed node metrics. + Debugging/Testing options: -debug= @@ -492,7 +496,8 @@ class ShowHelpTest(BitcoinTestFramework): assert_equal(process.returncode, 0) log_stdout.seek(0) stdout = log_stdout.read().decode('utf-8') - assert_true(help_message in stdout) + assert_true(help_message_1 in stdout) + assert_true(help_message_2 in stdout) def run_test(self): self.show_help() diff --git a/qa/rpc-tests/test_framework/netutil.py b/qa/rpc-tests/test_framework/netutil.py index 7aa8a51a6..98f099c55 100644 --- a/qa/rpc-tests/test_framework/netutil.py +++ b/qa/rpc-tests/test_framework/netutil.py @@ -107,7 +107,7 @@ def all_interfaces(): max_possible *= 2 else: break - namestr = names.tostring() + namestr = names.tobytes() return [(namestr[i:i+16].split(b'\0', 1)[0], socket.inet_ntoa(namestr[i+20:i+24])) for i in range(0, outbytes, struct_size)] diff --git a/qa/rpc-tests/test_framework/test_framework.py b/qa/rpc-tests/test_framework/test_framework.py index a1be92837..431825260 100755 --- a/qa/rpc-tests/test_framework/test_framework.py +++ b/qa/rpc-tests/test_framework/test_framework.py @@ -56,17 +56,18 @@ class BitcoinTestFramework(object): # Connect the nodes as a "chain". This allows us # to split the network between nodes 1 and 2 to get # two halves that can work on competing chains. + connect_nodes_bi(self.nodes, 0, 1) # If we joined network halves, connect the nodes from the joint # on outward. This ensures that chains are properly reorganised. - if not split: - connect_nodes_bi(self.nodes, 1, 2) - sync_blocks(self.nodes[1:3]) - if do_mempool_sync: - sync_mempools(self.nodes[1:3]) + if len(self.nodes) >= 4: + connect_nodes_bi(self.nodes, 2, 3) + if not split: + connect_nodes_bi(self.nodes, 1, 2) + sync_blocks(self.nodes[1:3]) + if do_mempool_sync: + sync_mempools(self.nodes[1:3]) - connect_nodes_bi(self.nodes, 0, 1) - connect_nodes_bi(self.nodes, 2, 3) self.is_network_split = split self.sync_all(do_mempool_sync) diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index 20c732349..143ff9027 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -214,7 +214,7 @@ def wait_for_bitcoind_start(process, url, i): ''' while True: if process.poll() is not None: - raise Exception('%s exited with status %i during initialization' % (zcashd_binary(), process.returncode)) + raise Exception('%s node %d exited with status %i during initialization' % (zcashd_binary(), i, process.returncode)) try: rpc = get_rpc_proxy(url, i) rpc.getblockcount() @@ -307,15 +307,15 @@ def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'): wait_bitcoinds() for i in range(MAX_NODES): # record the system time at which the cache was regenerated - with open(log_filename(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file: + with open(node_file(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file: cache_config = { "cache_time": time.time() } cache_conf_json = json.dumps(cache_config, indent=4) cache_conf_file.write(cache_conf_json) - os.remove(log_filename(cachedir, i, "debug.log")) - os.remove(log_filename(cachedir, i, "db.log")) - os.remove(log_filename(cachedir, i, "peers.dat")) - os.remove(log_filename(cachedir, i, "fee_estimates.dat")) + os.remove(node_file(cachedir, i, "debug.log")) + os.remove(node_file(cachedir, i, "db.log")) + os.remove(node_file(cachedir, i, "peers.dat")) + os.remove(node_file(cachedir, i, "fee_estimates.dat")) def init_from_cache(): for i in range(num_nodes): @@ -351,7 +351,7 @@ def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'): for i in range(MAX_NODES): node_path = os.path.join(cachedir, 'node'+str(i)) if os.path.isdir(node_path): - if not os.path.isfile(log_filename(cachedir, i, 'cache_config.json')): + if not os.path.isfile(node_file(cachedir, i, 'cache_config.json')): return True else: return True @@ -433,7 +433,7 @@ def assert_start_raises_init_error(i, dirname, extra_args=None, expected_msg=Non node = start_node(i, dirname, extra_args, stderr=log_stderr) stop_node(node, i) except Exception as e: - assert ("%s exited" % (zcashd_binary(),)) in str(e) #node must have shutdown + assert ("%s node %d exited" % (zcashd_binary(), i)) in str(e) # node must have shutdown if expected_msg is not None: log_stderr.seek(0) stderr = log_stderr.read().decode('utf-8') @@ -461,8 +461,8 @@ def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None): raise return rpcs -def log_filename(dirname, n_node, logname): - return os.path.join(dirname, "node"+str(n_node), "regtest", logname) +def node_file(dirname, n_node, filename): + return os.path.join(dirname, "node"+str(n_node), "regtest", filename) def check_node(i): bitcoind_processes[i].poll() diff --git a/qa/rpc-tests/wallet_accounts.py b/qa/rpc-tests/wallet_accounts.py index 68ee79bb5..459794a60 100755 --- a/qa/rpc-tests/wallet_accounts.py +++ b/qa/rpc-tests/wallet_accounts.py @@ -126,7 +126,7 @@ class WalletAccountsTest(BitcoinTestFramework): # The UA contains the expected receiver kinds. self.check_receiver_types(ua0, ['p2pkh', 'sapling', 'orchard']) self.check_receiver_types(ua0_2, ['p2pkh', 'sapling', 'orchard']) - self.check_receiver_types(ua0_3, [ 'sapling', 'orchard']) + self.check_receiver_types(ua0_3, [ 'sapling', 'orchard']) self.check_receiver_types(ua0_4, ['p2pkh', 'orchard']) self.check_receiver_types(ua1, ['p2pkh', 'sapling', 'orchard']) diff --git a/qa/rpc-tests/wallet_listunspent.py b/qa/rpc-tests/wallet_listunspent.py new file mode 100755 index 000000000..2514c88f2 --- /dev/null +++ b/qa/rpc-tests/wallet_listunspent.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-2022 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + get_coinbase_address, + nuparams, + start_nodes, + wait_and_assert_operationid_status, + NU5_BRANCH_ID +) + +from decimal import Decimal + +def unspent_total(unspent): + return sum((item['amount'] for item in unspent)) + +class WalletListUnspent(BitcoinTestFramework): + def setup_nodes(self): + return start_nodes(4, self.options.tmpdir, [[ + nuparams(NU5_BRANCH_ID, 201), + ]] * 4) + + def matured_at_height(self, height): + return unspent_total(self.nodes[0].listunspent(1, 999999, [], False, {}, height)) + + def run_test(self): + def expected_matured_at_height(height): + return (height-200)*10 + 250 + + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(200)) + assert_equal(self.nodes[1].getbalance(), expected_matured_at_height(200)) + + # Activate NU5 + self.nodes[1].generate(1) # height 201 + self.sync_all() + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(201)) + # check balances from before the latest tx + assert_equal(self.nodes[0].getbalance("", 1, False, False, 200), expected_matured_at_height(200)) + assert_equal(self.matured_at_height(200), expected_matured_at_height(200)) + + # Shield some coinbase funds so that they become spendable + n1acct = self.nodes[1].z_getnewaccount()['account'] + n1uaddr = self.nodes[1].z_getaddressforaccount(n1acct)['address'] + opid = self.nodes[0].z_sendmany( + get_coinbase_address(self.nodes[0]), + [{'address': n1uaddr, 'amount': 10}], + 1, 0, 'AllowRevealedSenders') + wait_and_assert_operationid_status(self.nodes[0], opid) + + self.sync_all() + self.nodes[1].generate(2) + self.sync_all() # height 203 + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(203) - 10) + + assert_equal( + self.nodes[1].z_getbalanceforaccount(n1acct, 1)['pools']['orchard']['valueZat'], + Decimal('1000000000')) + + # Get a bare legacy transparent address for node 0 + n0addr = self.nodes[0].getnewaddress() + + # Send funds to the node 0 address so we have transparent funds to spend. + opid = self.nodes[1].z_sendmany( + n1uaddr, + [{'address': n0addr, 'amount': 10}], + 1, 0, 'AllowRevealedRecipients') + wait_and_assert_operationid_status(self.nodes[1], opid) + + self.sync_all() + self.nodes[1].generate(2) + self.sync_all() # height 205 + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(205) - 10 + 10) + + # We will then perform several spends, and then check the list of + # unspent notes as of various heights. + + opid = self.nodes[0].z_sendmany( + 'ANY_TADDR', + # FIXME: #6262 The amount here _should_ be 2, but because of a + # bug in the selector for `ANY_TADDR`, it’s selecting + # transparent coinbase, which also means we can’t have + # change. When that bug is fixed, the test should fail + # here, and we can switch it back to 2 (and cascade the + # corrected amounts mentioned below. + [{'address': n1uaddr, 'amount': 10}], + 1, 0, 'AllowRevealedSenders') + wait_and_assert_operationid_status(self.nodes[0], opid) + + self.nodes[0].generate(2) + self.sync_all() # height 207 + # FIXME: #6262, should be `expected_matured_at_height(207) - 10 + 10 - 2` + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(207) - 10 + 10 - 10) + + opid = self.nodes[0].z_sendmany( + 'ANY_TADDR', + # FIXME: Should be 3 (see above) + [{'address': n1uaddr, 'amount': 10}], + 1, 0, 'AllowRevealedSenders') + wait_and_assert_operationid_status(self.nodes[0], opid) + + self.nodes[0].generate(2) + self.sync_all() # height 209 + # FIXME: #6262, should be `expected_matured_at_height(209) - 10 + 10 - 2 - 3` + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(209) - 10 + 10 - 10 - 10) + + opid = self.nodes[0].z_sendmany( + 'ANY_TADDR', + # FIXME: Should be 5 (see above) + [{'address': n1uaddr, 'amount': 10}], + 1, 0, 'AllowRevealedSenders') + wait_and_assert_operationid_status(self.nodes[0], opid) + + self.nodes[0].generate(2) + self.sync_all() # height 211 + # FIXME: #6262, should be `expected_matured_at_height(211) - 10 + 10 - 2 - 3 - 5` + assert_equal(self.nodes[0].getbalance(), expected_matured_at_height(211) - 10 + 10 - 10 - 10 - 10) + + # check balances at various past points in the chain + # FIXME: #6262, change the comparison amounts when the above changes are made. + assert_equal(self.matured_at_height(205), expected_matured_at_height(205) - 10 + 10) + assert_equal(self.matured_at_height(207), expected_matured_at_height(207) - 10 + 10 - 10) + assert_equal(self.matured_at_height(209), expected_matured_at_height(209) - 10 + 10 - 10 - 10) + assert_equal(self.matured_at_height(211), expected_matured_at_height(211) - 10 + 10 - 10 - 10 - 10) + +if __name__ == '__main__': + WalletListUnspent().main() diff --git a/qa/supply-chain/config.toml b/qa/supply-chain/config.toml index e14c25c9e..621bbf538 100644 --- a/qa/supply-chain/config.toml +++ b/qa/supply-chain/config.toml @@ -51,6 +51,10 @@ criteria = "safe-to-deploy" version = "0.7.6" criteria = "safe-to-deploy" +[[exemptions.aho-corasick]] +version = "0.7.19" +criteria = "safe-to-deploy" + [[exemptions.anyhow]] version = "1.0.56" criteria = "safe-to-deploy" diff --git a/qa/zcash/full_test_suite.py b/qa/zcash/full_test_suite.py index fd67a6945..119a137a1 100755 --- a/qa/zcash/full_test_suite.py +++ b/qa/zcash/full_test_suite.py @@ -137,12 +137,8 @@ def ensure_no_dot_so_in_depends(): return exit_code == 0 def util_test(): - python = [] - if os.path.isfile('/usr/local/bin/python3'): - python = ['/usr/local/bin/python3'] - return subprocess.call( - python + [repofile('src/test/bitcoin-util-test.py')], + [sys.executable, repofile('src/test/bitcoin-util-test.py')], cwd=repofile('src'), env={'PYTHONPATH': repofile('src/test'), 'srcdir': repofile('src')} ) == 0 diff --git a/qa/zcash/postponed-updates.txt b/qa/zcash/postponed-updates.txt index 139cdf904..f26d4b7ee 100644 --- a/qa/zcash/postponed-updates.txt +++ b/qa/zcash/postponed-updates.txt @@ -4,38 +4,59 @@ # bdb 18.1.40 2020-09-01 # +native_cxxbridge 1.0.80 2022-12-10 +native_cxxbridge 1.0.81 2022-12-10 +native_cxxbridge 1.0.82 2022-12-10 +native_cxxbridge 1.0.83 2022-12-10 +native_rust 1.65.0 2022-12-10 +rustcxx 1.0.80 2022-12-10 +rustcxx 1.0.81 2022-12-10 +rustcxx 1.0.82 2022-12-10 +rustcxx 1.0.83 2022-12-10 +utfcpp 3.2.2 2022-12-10 + # Ccache 4.0 requires adding CMake to the depends system. -native_ccache 4.0 2022-11-01 -native_ccache 4.1 2022-11-01 -native_ccache 4.2 2022-11-01 -native_ccache 4.2.1 2022-11-01 -native_ccache 4.3 2022-11-01 -native_ccache 4.4 2022-11-01 -native_ccache 4.4.1 2022-11-01 -native_ccache 4.4.2 2022-11-01 -native_ccache 4.5 2022-11-01 -native_ccache 4.5.1 2022-11-01 -native_ccache 4.6 2022-11-01 -native_ccache 4.6.1 2022-11-01 -native_ccache 4.6.2 2022-11-01 -native_ccache 4.6.3 2022-11-01 -native_ccache 4.7 2022-11-01 +native_ccache 4.0 2022-12-10 +native_ccache 4.1 2022-12-10 +native_ccache 4.2 2022-12-10 +native_ccache 4.2.1 2022-12-10 +native_ccache 4.3 2022-12-10 +native_ccache 4.4 2022-12-10 +native_ccache 4.4.1 2022-12-10 +native_ccache 4.4.2 2022-12-10 +native_ccache 4.5 2022-12-10 +native_ccache 4.5.1 2022-12-10 +native_ccache 4.6 2022-12-10 +native_ccache 4.6.1 2022-12-10 +native_ccache 4.6.2 2022-12-10 +native_ccache 4.6.3 2022-12-10 +native_ccache 4.7 2022-12-10 +native_ccache 4.7.1 2022-12-10 +native_ccache 4.7.2 2022-12-10 +native_ccache 4.7.3 2022-12-10 +native_ccache 4.7.4 2022-12-10 # Clang and Rust are currently pinned to LLVM 14 -libcxx 15.0.0 2022-11-01 -libcxx 15.0.1 2022-11-01 -libcxx 15.0.2 2022-11-01 -libcxx 15.0.3 2022-11-01 -native_clang 15.0.0 2022-11-01 -native_clang 15.0.1 2022-11-01 -native_clang 15.0.2 2022-11-01 -native_clang 15.0.3 2022-11-01 +libcxx 15.0.0 2022-12-10 +libcxx 15.0.1 2022-12-10 +libcxx 15.0.2 2022-12-10 +libcxx 15.0.3 2022-12-10 +libcxx 15.0.4 2022-12-10 +libcxx 15.0.5 2022-12-10 +libcxx 15.0.6 2022-12-10 +native_clang 15.0.0 2022-12-10 +native_clang 15.0.1 2022-12-10 +native_clang 15.0.2 2022-12-10 +native_clang 15.0.3 2022-12-10 +native_clang 15.0.4 2022-12-10 +native_clang 15.0.5 2022-12-10 +native_clang 15.0.6 2022-12-10 # We're never updating to this version bdb 18.1.40 2024-02-01 # Google Test 1.10.0 requires adding CMake to the depends system. -googletest 1.10.0 2022-11-01 -googletest 1.11.0 2022-11-01 -googletest 1.12.0 2022-11-01 -googletest 1.12.1 2022-11-01 +googletest 1.10.0 2022-12-10 +googletest 1.11.0 2022-12-10 +googletest 1.12.0 2022-12-10 +googletest 1.12.1 2022-12-10 diff --git a/src/chain.cpp b/src/chain.cpp index 9ab62b35e..d49d2d733 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -6,6 +6,11 @@ #include "chain.h" +#include "main.h" +#include "txdb.h" + +#include + /** * CChain implementation */ @@ -58,6 +63,50 @@ const CBlockIndex *CChain::FindFork(const CBlockIndex *pindex) const { return pindex; } +void CBlockIndex::TrimSolution() +{ + AssertLockHeld(cs_main); + + // We can correctly trim a solution as soon as the block index entry has been added + // to leveldb. Updates to the block index entry (to update validity status) will be + // handled by re-reading the solution from the existing db entry. It does not help to + // try to avoid these reads by gating trimming on the validity status: the re-reads are + // efficient anyway because of caching in leveldb, and most of them are unavoidable. + if (HasSolution()) { + MetricsIncrementCounter("zcashd.debug.memory.trimmed_equihash_solutions"); + std::vector empty; + nSolution.swap(empty); + } +} + +CBlockHeader CBlockIndex::GetBlockHeader() const +{ + AssertLockHeld(cs_main); + + CBlockHeader header; + header.nVersion = nVersion; + if (pprev) { + header.hashPrevBlock = pprev->GetBlockHash(); + } + header.hashMerkleRoot = hashMerkleRoot; + header.hashBlockCommitments = hashBlockCommitments; + header.nTime = nTime; + header.nBits = nBits; + header.nNonce = nNonce; + if (HasSolution()) { + header.nSolution = nSolution; + } else { + MetricsIncrementCounter("zcashd.debug.blocktree.trimmed_equihash_read_dbindex"); + CDiskBlockIndex dbindex; + if (!pblocktree->ReadDiskBlockIndex(GetBlockHash(), dbindex)) { + LogPrintf("%s: Failed to read index entry", __func__); + throw std::runtime_error("Failed to read index entry"); + } + header.nSolution = dbindex.GetSolution(); + } + return header; +} + /** Turn the lowest '1' bit in the binary representation of a number into a '0'. */ int static inline InvertLowestOne(int n) { return n & (n - 1); } diff --git a/src/chain.h b/src/chain.h index 65faca6c6..22de203d0 100644 --- a/src/chain.h +++ b/src/chain.h @@ -12,10 +12,13 @@ #include "pow.h" #include "tinyformat.h" #include "uint256.h" +#include "util/strencodings.h" #include #include +#include + static const int SPROUT_VALUE_VERSION = 1001400; static const int SAPLING_VALUE_VERSION = 1010100; static const int CHAIN_HISTORY_ROOT_VERSION = 2010200; @@ -308,8 +311,13 @@ public: unsigned int nTime; unsigned int nBits; uint256 nNonce; +protected: + // The Equihash solution, if it is stored. Once we know that the block index + // entry is present in leveldb, this field can be cleared via the TrimSolution + // method to save memory. std::vector nSolution; +public: //! (memory only) Sequential id assigned to distinguish order in which blocks are received. uint32_t nSequenceId; @@ -366,6 +374,7 @@ public: nBits = block.nBits; nNonce = block.nNonce; nSolution = block.nSolution; + MetricsIncrementCounter("zcashd.debug.memory.allocated_equihash_solutions"); } CDiskBlockPos GetBlockPos() const { @@ -386,23 +395,15 @@ public: return ret; } - CBlockHeader GetBlockHeader() const - { - CBlockHeader block; - block.nVersion = nVersion; - if (pprev) - block.hashPrevBlock = pprev->GetBlockHash(); - block.hashMerkleRoot = hashMerkleRoot; - block.hashBlockCommitments = hashBlockCommitments; - block.nTime = nTime; - block.nBits = nBits; - block.nNonce = nNonce; - block.nSolution = nSolution; - return block; - } + //! Get the block header for this block index. Requires cs_main. + CBlockHeader GetBlockHeader() const; + + //! Clear the Equihash solution to save memory. Requires cs_main. + void TrimSolution(); uint256 GetBlockHash() const { + assert(phashBlock); return *phashBlock; } @@ -429,10 +430,11 @@ public: std::string ToString() const { - return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s, hashBlock=%s)", + return strprintf("CBlockIndex(pprev=%p, nHeight=%d, merkle=%s, hashBlock=%s, HasSolution=%s)", pprev, nHeight, hashMerkleRoot.ToString(), - GetBlockHash().ToString()); + phashBlock ? GetBlockHash().ToString() : "(nil)", + HasSolution()); } //! Check whether this block index entry is valid up to the passed validity level. @@ -444,6 +446,12 @@ public: return ((nStatus & BLOCK_VALID_MASK) >= nUpTo); } + //! Is the Equihash solution stored? + bool HasSolution() const + { + return !nSolution.empty(); + } + //! Raise the validity level of this block index entry. //! Returns true if the validity was changed. bool RaiseValidity(enum BlockStatus nUpTo) @@ -482,8 +490,11 @@ public: hashPrev = uint256(); } - explicit CDiskBlockIndex(const CBlockIndex* pindex) : CBlockIndex(*pindex) { + explicit CDiskBlockIndex(const CBlockIndex* pindex, std::function()> getSolution) : CBlockIndex(*pindex) { hashPrev = (pprev ? pprev->GetBlockHash() : uint256()); + if (!HasSolution()) { + nSolution = getSolution(); + } } ADD_SERIALIZE_METHODS; @@ -564,20 +575,31 @@ public: // them to CBlockTreeDB::LoadBlockIndexGuts() in txdb.cpp :) } - uint256 GetBlockHash() const + //! Get the block header for this block index. + CBlockHeader GetBlockHeader() const { - CBlockHeader block; - block.nVersion = nVersion; - block.hashPrevBlock = hashPrev; - block.hashMerkleRoot = hashMerkleRoot; - block.hashBlockCommitments = hashBlockCommitments; - block.nTime = nTime; - block.nBits = nBits; - block.nNonce = nNonce; - block.nSolution = nSolution; - return block.GetHash(); + CBlockHeader header; + header.nVersion = nVersion; + header.hashPrevBlock = hashPrev; + header.hashMerkleRoot = hashMerkleRoot; + header.hashBlockCommitments = hashBlockCommitments; + header.nTime = nTime; + header.nBits = nBits; + header.nNonce = nNonce; + header.nSolution = nSolution; + return header; } + uint256 GetBlockHash() const + { + return GetBlockHeader().GetHash(); + } + + std::vector GetSolution() const + { + assert(HasSolution()); + return nSolution; + } std::string ToString() const { @@ -588,6 +610,13 @@ public: hashPrev.ToString()); return str; } + +private: + //! This method should not be called on a CDiskBlockIndex. + void TrimSolution() + { + assert(!"called CDiskBlockIndex::TrimSolution"); + } }; /** An in-memory indexed chain of blocks. */ diff --git a/src/clientversion.h b/src/clientversion.h index 3bd546896..be677a81f 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -17,7 +17,7 @@ //! These need to be macros, as clientversion.cpp's and bitcoin*-res.rc's voodoo requires it #define CLIENT_VERSION_MAJOR 5 #define CLIENT_VERSION_MINOR 3 -#define CLIENT_VERSION_REVISION 0 +#define CLIENT_VERSION_REVISION 2 #define CLIENT_VERSION_BUILD 50 //! Set to true for release, false for prerelease or test build diff --git a/src/cuckoocache.h b/src/cuckoocache.h index 14bcd98c6..e34ddd18e 100644 --- a/src/cuckoocache.h +++ b/src/cuckoocache.h @@ -281,7 +281,7 @@ private: * * First, epoch_check decrements and checks the cheap heuristic, and then does * a more expensive scan if the cheap heuristic runs out. If the expensive - * scan suceeds, the epochs are aged and old elements are allow_erased. The + * scan succeeds, the epochs are aged and old elements are allow_erased. The * cheap heuristic is reset to retrigger after the worst case growth of the * current epoch's elements would exceed the epoch_size. */ diff --git a/src/deprecation.h b/src/deprecation.h index 411649a8f..4a8f7a558 100644 --- a/src/deprecation.h +++ b/src/deprecation.h @@ -10,7 +10,7 @@ // Per https://zips.z.cash/zip-0200 // Shut down nodes running this version of code, 16 weeks' worth of blocks after the estimated // release block height. A warning is shown during the 14 days' worth of blocks prior to shut down. -static const int APPROX_RELEASE_HEIGHT = 1849900; +static const int APPROX_RELEASE_HEIGHT = 1900500; static const int RELEASE_TO_DEPRECATION_WEEKS = 16; static const int EXPECTED_BLOCKS_PER_HOUR = 3600 / Consensus::POST_BLOSSOM_POW_TARGET_SPACING; static_assert(EXPECTED_BLOCKS_PER_HOUR == 48, "The value of Consensus::POST_BLOSSOM_POW_TARGET_SPACING was chosen such that this assertion holds."); diff --git a/src/init.cpp b/src/init.cpp index 74b1d39f6..1072f64d5 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -438,6 +438,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-prometheusport=", _("Expose node metrics in the Prometheus exposition format. " "An HTTP listener will be started on , which responds to GET requests on any request path. " "Use -metricsallowip and -metricsbind to control access.")); + strUsage += HelpMessageOpt("-debugmetrics", _("Include debug metrics in exposed node metrics.")); strUsage += HelpMessageGroup(_("Debugging/Testing options:")); if (showDebug) @@ -1471,10 +1472,12 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) metricsBindCstr = metricsBind.c_str(); } + bool debugMetrics = GetBoolArg("-debugmetrics", false); + // Start up the metrics runtime. This spins off a Rust thread that runs // the Prometheus exporter. We just let this thread die at process end. LogPrintf("metrics thread start"); - if (!metrics_run(metricsBindCstr, vAllowCstr.data(), vAllowCstr.size(), prometheusPort)) { + if (!metrics_run(metricsBindCstr, vAllowCstr.data(), vAllowCstr.size(), prometheusPort, debugMetrics)) { return InitError(strprintf(_("Failed to start Prometheus metrics exporter"))); } } diff --git a/src/main.cpp b/src/main.cpp index b3ba4d89c..a4b8ffe81 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3715,7 +3715,7 @@ bool static FlushStateToDisk( vFiles.push_back(make_pair(*it, &vinfoBlockFile[*it])); it = setDirtyFileInfo.erase(it); } - std::vector vBlocks; + std::vector vBlocks; vBlocks.reserve(setDirtyBlockIndex.size()); for (set::iterator it = setDirtyBlockIndex.begin(); it != setDirtyBlockIndex.end(); ) { vBlocks.push_back(*it); @@ -3724,6 +3724,12 @@ bool static FlushStateToDisk( if (!pblocktree->WriteBatchSync(vFiles, nLastBlockFile, vBlocks)) { return AbortNode(state, "Files to write to block index database"); } + // Now that we have written the block indices to the database, we do not + // need to store solutions for these CBlockIndex objects in memory. + // cs_main must be held here. + for (CBlockIndex *pblockindex : vBlocks) { + pblockindex->TrimSolution(); + } } // Finally remove any pruned files if (fFlushForPrune) @@ -6128,7 +6134,11 @@ void static CheckBlockIndex(const Consensus::Params& consensusParams) } } } - // assert(pindex->GetBlockHash() == pindex->GetBlockHeader().GetHash()); // Perhaps too slow + // try { + // assert(pindex->GetBlockHash() == pindex->GetBlockHeader().GetHash()); // Perhaps too slow + // } catch (const runtime_error&) { + // assert(!"Failed to read index entry"); + // } // End: actual consistency checks. // Try descending into the first subnode. diff --git a/src/rest.cpp b/src/rest.cpp index 29db4a6ba..83d0313ee 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -141,6 +141,7 @@ static bool rest_headers(HTTPRequest* req, std::vector headers; headers.reserve(count); + CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION); { LOCK(cs_main); BlockMap::const_iterator it = mapBlockIndex.find(hash); @@ -151,11 +152,16 @@ static bool rest_headers(HTTPRequest* req, break; pindex = chainActive.Next(pindex); } - } - CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION); - for (const CBlockIndex *pindex : headers) { - ssHeader << pindex->GetBlockHeader(); + if (rf == RF_BINARY || rf == RF_HEX) { + try { + for (const CBlockIndex *pindex : headers) { + ssHeader << pindex->GetBlockHeader(); + } + } catch (const std::runtime_error&) { + return RESTERR(req, HTTP_INTERNAL_SERVER_ERROR, "Failed to read index entry"); + } + } } switch (rf) { diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index a929d1c19..ae35e09c4 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -118,7 +118,7 @@ UniValue blockheaderToJSON(const CBlockIndex* blockindex) result.pushKV("finalsaplingroot", blockindex->hashFinalSaplingRoot.GetHex()); result.pushKV("time", (int64_t)blockindex->nTime); result.pushKV("nonce", blockindex->nNonce.GetHex()); - result.pushKV("solution", HexStr(blockindex->nSolution)); + result.pushKV("solution", HexStr(blockindex->GetBlockHeader().nSolution)); result.pushKV("bits", strprintf("%08x", blockindex->nBits)); result.pushKV("difficulty", GetDifficulty(blockindex)); result.pushKV("chainwork", blockindex->nChainWork.GetHex()); @@ -684,15 +684,18 @@ UniValue getblockheader(const UniValue& params, bool fHelp) CBlockIndex* pblockindex = mapBlockIndex[hash]; - if (!fVerbose) - { - CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION); - ssBlock << pblockindex->GetBlockHeader(); - std::string strHex = HexStr(ssBlock.begin(), ssBlock.end()); - return strHex; + try { + if (!fVerbose) { + CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION); + ssBlock << pblockindex->GetBlockHeader(); + std::string strHex = HexStr(ssBlock.begin(), ssBlock.end()); + return strHex; + } else { + return blockheaderToJSON(pblockindex); + } + } catch (const runtime_error&) { + throw JSONRPCError(RPC_DATABASE_ERROR, "Failed to read index entry"); } - - return blockheaderToJSON(pblockindex); } UniValue getblock(const UniValue& params, bool fHelp) diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index f55eab30e..a29815a46 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -93,7 +93,7 @@ UniValue getinfo(const UniValue& params, bool fHelp) #ifdef ENABLE_WALLET if (pwalletMain) { obj.pushKV("walletversion", pwalletMain->GetVersion()); - obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance())); + obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance(std::nullopt))); } #endif obj.pushKV("blocks", (int)chainActive.Height()); diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 62867c03d..1671a90a6 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -550,6 +550,55 @@ std::string experimentalDisabledHelpMsg(const std::string& rpc, const std::vecto + config; } +std::string asOfHeightMessage(bool hasMinconf) { + std::string minconfInteraction = hasMinconf + ? " `minconf` must be at least 1 when `asOfHeight` is provided.\n" + : ""; + return + "asOfHeight (numeric, optional, default=-1) Execute the query as if it\n" + " were run when the blockchain was at the height specified by\n" + " this argument. The default is to use the entire blockchain\n" + " that the node is aware of. -1 can be used as in other RPC\n" + " calls to indicate the current height (including the\n" + " mempool), but this does not support negative values in\n" + " general. A “future” height will fall back to the current\n" + " height. Any explicit value will cause the mempool to be\n" + " ignored, meaning no unconfirmed tx will be considered.\n" + + minconfInteraction; +} + +std::optional parseAsOfHeight(const UniValue& params, int index) { + std::optional asOfHeight; + if (params.size() > index) { + auto requestedHeight = params[index].get_int(); + if (requestedHeight == -1) { + // the default, do nothing + } else if (requestedHeight < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Can not perform the query as of a negative block height"); + } else if (requestedHeight == 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Can not perform the query as of the genesis block"); + } else { + asOfHeight = requestedHeight; + } + } + return asOfHeight; +} + +int parseMinconf(int defaultValue, const UniValue& params, int index, const std::optional& asOfHeight) { + int nMinDepth = defaultValue; + if (params.size() > index) { + auto requestedDepth = params[index].get_int(); + if (requestedDepth < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); + } else if (requestedDepth == 0 && asOfHeight.has_value()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Require a minimum of 1 confirmation when `asOfHeight` is provided"); + } else { + nMinDepth = requestedDepth; + } + } + return nMinDepth; +} + void RPCRegisterTimerInterface(RPCTimerInterface *iface) { timerInterfaces.push_back(iface); diff --git a/src/rpc/server.h b/src/rpc/server.h index 137a02123..c06610c79 100644 --- a/src/rpc/server.h +++ b/src/rpc/server.h @@ -192,6 +192,10 @@ std::string JSONRPCExecBatch(const UniValue& vReq); extern std::string experimentalDisabledHelpMsg(const std::string& rpc, const std::vector& enableArgs); +std::string asOfHeightMessage(bool hasMinconf); +std::optional parseAsOfHeight(const UniValue& params, int index); +int parseMinconf(int defaultValue, const UniValue& params, int index, const std::optional& asOfHeight); + extern int interpretHeightArg(int nHeight, int currentHeight); extern int parseHeightArg(const std::string& strHeight, int currentHeight); diff --git a/src/rust/include/rust/metrics.h b/src/rust/include/rust/metrics.h index 86c1bfd4a..2fe86ad5c 100644 --- a/src/rust/include/rust/metrics.h +++ b/src/rust/include/rust/metrics.h @@ -24,7 +24,8 @@ bool metrics_run( const char* bind_address, const char* const* allow_ips, size_t allow_ips_len, - uint16_t prometheus_port); + uint16_t prometheus_port, + bool debug_metrics); struct MetricsCallsite; typedef struct MetricsCallsite MetricsCallsite; diff --git a/src/rust/src/metrics_ffi.rs b/src/rust/src/metrics_ffi.rs index 169f82da8..8cf995eb7 100644 --- a/src/rust/src/metrics_ffi.rs +++ b/src/rust/src/metrics_ffi.rs @@ -1,18 +1,48 @@ use libc::{c_char, c_double}; use metrics::{try_recorder, Key, Label}; -use metrics_exporter_prometheus::PrometheusBuilder; +use metrics_exporter_prometheus::{BuildError, PrometheusBuilder}; +use metrics_util::layers::{FilterLayer, Stack}; + use std::ffi::CStr; use std::net::{IpAddr, SocketAddr}; use std::ptr; use std::slice; +use std::thread; + use tracing::error; +/// Builds the recorder and exporter, applies the given filters to the recorder, and +/// installs both of them globally. +fn metrics_install(builder: PrometheusBuilder, filters: &[&str]) -> Result<(), BuildError> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| BuildError::FailedToCreateRuntime(e.to_string()))?; + + let (recorder, exporter) = { + let _g = runtime.enter(); + builder.build()? + }; + + thread::Builder::new() + .name("zc-metrics".to_string()) + .spawn(move || runtime.block_on(exporter)) + .map_err(|e| BuildError::FailedToCreateRuntime(e.to_string()))?; + + Stack::new(recorder) + .push(FilterLayer::from_patterns(filters)) + .install()?; + + Ok(()) +} + #[no_mangle] pub extern "C" fn metrics_run( bind_address: *const c_char, allow_ips: *const *const c_char, allow_ips_len: usize, prometheus_port: u16, + debug_metrics: bool, ) -> bool { // Convert the C string IPs to Rust strings. let allow_ips = unsafe { slice::from_raw_parts(allow_ips, allow_ips_len) }; @@ -54,6 +84,13 @@ pub extern "C" fn metrics_run( prometheus_port, ); + // Metrics matching any of these filters will be discarded. + let filters = if debug_metrics { + &[][..] + } else { + &["zcashd.debug."] + }; + allow_ips .into_iter() .try_fold( @@ -66,7 +103,7 @@ pub extern "C" fn metrics_run( }, ) .and_then(|builder| { - builder.install().map_err(|e| { + metrics_install(builder, filters).map_err(|e| { error!("Could not install Prometheus exporter: {}", e); e }) diff --git a/src/test/merkle_tests.cpp b/src/test/merkle_tests.cpp index 20cacad8e..e2086a264 100644 --- a/src/test/merkle_tests.cpp +++ b/src/test/merkle_tests.cpp @@ -235,7 +235,7 @@ BOOST_AUTO_TEST_CASE(merkle_test) // If no mutation was done (once for every ntx value), try up to 16 branches. if (mutate == 0) { for (int loop = 0; loop < std::min(ntx, 16); loop++) { - // If ntx <= 16, try all branches. Otherise, try 16 random ones. + // If ntx <= 16, try all branches. Otherwise, try 16 random ones. int mtx = loop; if (ntx > 16) { mtx = InsecureRandRange(ntx); diff --git a/src/txdb.cpp b/src/txdb.cpp index c95c96846..e22533d0e 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -17,6 +17,8 @@ #include +#include + using namespace std; // NOTE: Per issue #3277, do not use the prefix 'X' or 'x' as they were @@ -316,7 +318,7 @@ bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, CBlockTreeDB::CBlockTreeDB(size_t nCacheSize, bool fMemory, bool fWipe) : CDBWrapper(GetDataDir() / "blocks" / "index", nCacheSize, fMemory, fWipe) { } -bool CBlockTreeDB::ReadBlockFileInfo(int nFile, CBlockFileInfo &info) { +bool CBlockTreeDB::ReadBlockFileInfo(int nFile, CBlockFileInfo &info) const { return Read(make_pair(DB_BLOCK_FILES, nFile), info); } @@ -327,12 +329,12 @@ bool CBlockTreeDB::WriteReindexing(bool fReindexing) { return Erase(DB_REINDEX_FLAG); } -bool CBlockTreeDB::ReadReindexing(bool &fReindexing) { +bool CBlockTreeDB::ReadReindexing(bool &fReindexing) const { fReindexing = Exists(DB_REINDEX_FLAG); return true; } -bool CBlockTreeDB::ReadLastBlockFile(int &nFile) { +bool CBlockTreeDB::ReadLastBlockFile(int &nFile) const { return Read(DB_LAST_BLOCK, nFile); } @@ -382,14 +384,31 @@ bool CCoinsViewDB::GetStats(CCoinsStats &stats) const { return true; } -bool CBlockTreeDB::WriteBatchSync(const std::vector >& fileInfo, int nLastFile, const std::vector& blockinfo) { +bool CBlockTreeDB::WriteBatchSync(const std::vector >& fileInfo, int nLastFile, const std::vector& blockinfo) { + MetricsIncrementCounter("zcashd.debug.blocktree.write_batch"); CDBBatch batch(*this); for (std::vector >::const_iterator it=fileInfo.begin(); it != fileInfo.end(); it++) { batch.Write(make_pair(DB_BLOCK_FILES, it->first), *it->second); } batch.Write(DB_LAST_BLOCK, nLastFile); for (std::vector::const_iterator it=blockinfo.begin(); it != blockinfo.end(); it++) { - batch.Write(make_pair(DB_BLOCK_INDEX, (*it)->GetBlockHash()), CDiskBlockIndex(*it)); + std::pair key = make_pair(DB_BLOCK_INDEX, (*it)->GetBlockHash()); + try { + CDiskBlockIndex dbindex {*it, [this, &key]() { + MetricsIncrementCounter("zcashd.debug.blocktree.write_batch_read_dbindex"); + // It can happen that the index entry is written, then the Equihash solution is cleared from memory, + // then the index entry is rewritten. In that case we must read the solution from the old entry. + CDiskBlockIndex dbindex_old; + if (!Read(key, dbindex_old)) { + LogPrintf("%s: Failed to read index entry", __func__); + throw runtime_error("Failed to read index entry"); + } + return dbindex_old.GetSolution(); + }}; + batch.Write(key, dbindex); + } catch (const runtime_error&) { + return false; + } } return WriteBatch(batch, true); } @@ -402,7 +421,11 @@ bool CBlockTreeDB::EraseBatchSync(const std::vector& blockin return WriteBatch(batch, true); } -bool CBlockTreeDB::ReadTxIndex(const uint256 &txid, CDiskTxPos &pos) { +bool CBlockTreeDB::ReadDiskBlockIndex(const uint256 &blockhash, CDiskBlockIndex &dbindex) const { + return Read(make_pair(DB_BLOCK_INDEX, blockhash), dbindex); +} + +bool CBlockTreeDB::ReadTxIndex(const uint256 &txid, CDiskTxPos &pos) const { return Read(make_pair(DB_TXINDEX, txid), pos); } @@ -491,7 +514,7 @@ bool CBlockTreeDB::ReadAddressIndex( return true; } -bool CBlockTreeDB::ReadSpentIndex(CSpentIndexKey &key, CSpentIndexValue &value) { +bool CBlockTreeDB::ReadSpentIndex(CSpentIndexKey &key, CSpentIndexValue &value) const { return Read(make_pair(DB_SPENTINDEX, key), value); } @@ -547,7 +570,7 @@ bool CBlockTreeDB::WriteTimestampBlockIndex(const CTimestampBlockIndexKey &block return WriteBatch(batch); } -bool CBlockTreeDB::ReadTimestampBlockIndex(const uint256 &hash, unsigned int <imestamp) +bool CBlockTreeDB::ReadTimestampBlockIndex(const uint256 &hash, unsigned int <imestamp) const { CTimestampBlockIndexValue(lts); if (!Read(std::make_pair(DB_BLOCKHASHINDEX, hash), lts)) @@ -562,7 +585,7 @@ bool CBlockTreeDB::WriteFlag(const std::string &name, bool fValue) { return Write(std::make_pair(DB_FLAG, name), fValue ? '1' : '0'); } -bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) { +bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) const { char ch; if (!Read(std::make_pair(DB_FLAG, name), ch)) return false; @@ -599,7 +622,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts( pindexNew->nTime = diskindex.nTime; pindexNew->nBits = diskindex.nBits; pindexNew->nNonce = diskindex.nNonce; - pindexNew->nSolution = diskindex.nSolution; + // the Equihash solution will be loaded lazily from the dbindex entry pindexNew->nStatus = diskindex.nStatus; pindexNew->nCachedBranchId = diskindex.nCachedBranchId; pindexNew->nTx = diskindex.nTx; @@ -612,10 +635,22 @@ bool CBlockTreeDB::LoadBlockIndexGuts( pindexNew->hashAuthDataRoot = diskindex.hashAuthDataRoot; // Consistency checks - auto header = pindexNew->GetBlockHeader(); + CBlockHeader header; + { + LOCK(cs_main); + try { + header = pindexNew->GetBlockHeader(); + } catch (const runtime_error&) { + return error("LoadBlockIndex(): failed to read index entry: diskindex hash = %s", + diskindex.GetBlockHash().ToString()); + } + } + if (header.GetHash() != diskindex.GetBlockHash()) + return error("LoadBlockIndex(): inconsistent header vs diskindex hash: header hash = %s, diskindex hash = %s", + header.GetHash().ToString(), diskindex.GetBlockHash().ToString()); if (header.GetHash() != pindexNew->GetBlockHash()) return error("LoadBlockIndex(): block header inconsistency detected: on-disk = %s, in-memory = %s", - diskindex.ToString(), pindexNew->ToString()); + diskindex.ToString(), pindexNew->ToString()); if (!CheckProofOfWork(pindexNew->GetBlockHash(), pindexNew->nBits, Params().GetConsensus())) return error("LoadBlockIndex(): CheckProofOfWork failed: %s", pindexNew->ToString()); diff --git a/src/txdb.h b/src/txdb.h index 07971805e..46bc694f8 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -117,13 +117,14 @@ private: CBlockTreeDB(const CBlockTreeDB&); void operator=(const CBlockTreeDB&); public: - bool WriteBatchSync(const std::vector >& fileInfo, int nLastFile, const std::vector& blockinfo); + bool WriteBatchSync(const std::vector >& fileInfo, int nLastFile, const std::vector& blockinfo); bool EraseBatchSync(const std::vector& blockinfo); - bool ReadBlockFileInfo(int nFile, CBlockFileInfo &info); - bool ReadLastBlockFile(int &nFile); + bool ReadBlockFileInfo(int nFile, CBlockFileInfo &info) const; + bool ReadLastBlockFile(int &nFile) const; bool WriteReindexing(bool fReindexing); - bool ReadReindexing(bool &fReindexing); - bool ReadTxIndex(const uint256 &txid, CDiskTxPos &pos); + bool ReadReindexing(bool &fReindexing) const; + bool ReadDiskBlockIndex(const uint256 &blockhash, CDiskBlockIndex &dbindex) const; + bool ReadTxIndex(const uint256 &txid, CDiskTxPos &pos) const; bool WriteTxIndex(const std::vector > &vect); // START insightexplorer @@ -132,18 +133,18 @@ public: bool WriteAddressIndex(const std::vector &vect); bool EraseAddressIndex(const std::vector &vect); bool ReadAddressIndex(uint160 addressHash, int type, std::vector &addressIndex, int start = 0, int end = 0); - bool ReadSpentIndex(CSpentIndexKey &key, CSpentIndexValue &value); + bool ReadSpentIndex(CSpentIndexKey &key, CSpentIndexValue &value) const; bool UpdateSpentIndex(const std::vector &vect); bool WriteTimestampIndex(const CTimestampIndexKey ×tampIndex); bool ReadTimestampIndex(unsigned int high, unsigned int low, const bool fActiveOnly, std::vector > &vect); bool WriteTimestampBlockIndex(const CTimestampBlockIndexKey &blockhashIndex, const CTimestampBlockIndexValue &logicalts); - bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS); + bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const; // END insightexplorer bool WriteFlag(const std::string &name, bool fValue); - bool ReadFlag(const std::string &name, bool &fValue); + bool ReadFlag(const std::string &name, bool &fValue) const; bool LoadBlockIndexGuts( std::function insertBlockIndex, const CChainParams& chainParams); diff --git a/src/wallet/asyncrpcoperation_mergetoaddress.cpp b/src/wallet/asyncrpcoperation_mergetoaddress.cpp index 49cafe976..cf3ae3a62 100644 --- a/src/wallet/asyncrpcoperation_mergetoaddress.cpp +++ b/src/wallet/asyncrpcoperation_mergetoaddress.cpp @@ -639,7 +639,7 @@ bool AsyncRPCOperation_mergetoaddress::main_impl() throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString())); } wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight; - wtxDepth = wtx.GetDepthInMainChain(); + wtxDepth = wtx.GetDepthInMainChain(std::nullopt); } LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vJoinSplit=%d, jsoutindex=%d, amount=%s, height=%d, confirmations=%d)\n", getId(), diff --git a/src/wallet/asyncrpcoperation_saplingmigration.cpp b/src/wallet/asyncrpcoperation_saplingmigration.cpp index 8d3b94c64..85abdf4c0 100644 --- a/src/wallet/asyncrpcoperation_saplingmigration.cpp +++ b/src/wallet/asyncrpcoperation_saplingmigration.cpp @@ -86,7 +86,7 @@ bool AsyncRPCOperation_saplingmigration::main_impl() { // We set minDepth to 11 to avoid unconfirmed notes and in anticipation of specifying // an anchor at height N-10 for each Sprout JoinSplit description // Consider, should notes be sorted? - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 11); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 11); } CAmount availableFunds = 0; for (const SproutNoteEntry& sproutEntry : sproutEntries) { diff --git a/src/wallet/asyncrpcoperation_sendmany.cpp b/src/wallet/asyncrpcoperation_sendmany.cpp index 6d4f45d6e..31dcc114f 100644 --- a/src/wallet/asyncrpcoperation_sendmany.cpp +++ b/src/wallet/asyncrpcoperation_sendmany.cpp @@ -192,7 +192,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() { SpendableInputs spendable; { LOCK2(cs_main, pwalletMain->cs_wallet); - spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_); + spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_, std::nullopt); } if (!spendable.LimitToAmount(targetAmount, dustThreshold, recipientPools_)) { CAmount changeAmount{spendable.Total() - targetAmount}; diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index 5cc93f97d..898e2db03 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -214,18 +214,18 @@ TEST(WalletTests, FindUnspentSproutNotes) { wtx.SetSproutNoteData(noteData); wallet.LoadWalletTx(wtx); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); // We currently have an unspent and unconfirmed note in the wallet (depth of -1) std::vector sproutEntries; std::vector saplingEntries; std::vector orchardEntries; - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, -1); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, -1); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); @@ -244,21 +244,21 @@ TEST(WalletTests, FindUnspentSproutNotes) { wtx.SetMerkleBranch(block); wallet.LoadWalletTx(wtx); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); // We now have an unspent and confirmed note in the wallet (depth of 1) - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 1); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); @@ -268,7 +268,7 @@ TEST(WalletTests, FindUnspentSproutNotes) { // Let's spend the note. auto wtx2 = GetValidSproutSpend(sk, note, 5); wallet.LoadWalletTx(wtx2); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); // Fake-mine a spend transaction EXPECT_EQ(0, chainActive.Height()); @@ -286,22 +286,22 @@ TEST(WalletTests, FindUnspentSproutNotes) { wtx2.SetMerkleBranch(block2); wallet.LoadWalletTx(wtx2); - EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); + EXPECT_TRUE(wallet.IsSproutSpent(nullifier, std::nullopt)); // The note has been spent. By default, GetFilteredNotes() ignores spent notes. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); // Let's include spent notes to retrieve it. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 0, INT_MAX, false); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); // The spent note has two confirmations. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2, INT_MAX, false); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); @@ -328,7 +328,7 @@ TEST(WalletTests, FindUnspentSproutNotes) { wtx.SetSproutNoteData(noteData); wallet.LoadWalletTx(wtx); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); wtx3 = wtx; } @@ -351,25 +351,25 @@ TEST(WalletTests, FindUnspentSproutNotes) { wallet.LoadWalletTx(wtx3); // We now have an unspent note which has one confirmation, in addition to our spent note. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 1); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); // Let's return the spent note too. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 1, INT_MAX, false); EXPECT_EQ(2, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); // Increasing number of confirmations will exclude our new unspent note. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2, INT_MAX, false); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); orchardEntries.clear(); // If we also ignore spent notes at this depth, we won't find any notes. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, true); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, 2, INT_MAX, true); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); @@ -877,7 +877,7 @@ TEST(WalletTests, GetConflictedOrchardNotes) { std::vector sproutEntries; std::vector saplingEntries; std::vector orchardEntries; - wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, -1); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, std::nullopt, -1); EXPECT_EQ(0, sproutEntries.size()); EXPECT_EQ(0, saplingEntries.size()); EXPECT_EQ(1, orchardEntries.size()); @@ -957,14 +957,14 @@ TEST(WalletTests, SproutNullifierIsSpent) { auto note = GetSproutNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); wallet.LoadWalletTx(wtx); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); auto wtx2 = GetValidSproutSpend(sk, note, 5); wallet.LoadWalletTx(wtx2); - EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier, std::nullopt)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); @@ -980,7 +980,7 @@ TEST(WalletTests, SproutNullifierIsSpent) { wtx2.SetMerkleBranch(block); wallet.LoadWalletTx(wtx2); - EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); + EXPECT_TRUE(wallet.IsSproutSpent(nullifier, std::nullopt)); // Tear down chainActive.SetTip(NULL); @@ -1018,7 +1018,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) { uint256 nullifier = nf.value(); // Verify note has not been spent - EXPECT_FALSE(wallet.IsSaplingSpent(nullifier)); + EXPECT_FALSE(wallet.IsSaplingSpent(nullifier, std::nullopt)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); @@ -1036,7 +1036,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) { wallet.LoadWalletTx(wtx); // Verify note has been spent - EXPECT_TRUE(wallet.IsSaplingSpent(nullifier)); + EXPECT_TRUE(wallet.IsSaplingSpent(nullifier, std::nullopt)); // Tear down chainActive.SetTip(NULL); @@ -1107,7 +1107,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) { MerkleFrontiers frontiers = { .sapling = testNote.tree }; // Verify dummy note is unspent - EXPECT_FALSE(wallet.IsSaplingSpent(nullifier)); + EXPECT_FALSE(wallet.IsSaplingSpent(nullifier, std::nullopt)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); @@ -1129,7 +1129,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) { wallet.LoadWalletTx(wtx); // Verify dummy note is now spent, as AddToWallet invokes AddToSpends() - EXPECT_TRUE(wallet.IsSaplingSpent(nullifier)); + EXPECT_TRUE(wallet.IsSaplingSpent(nullifier, std::nullopt)); // Test invariant: no witnesses means no nullifier. EXPECT_EQ(0, wallet.mapSaplingNullifiersToNotes.size()); @@ -1334,7 +1334,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) { wallet.LoadWalletTx(wtx2); // Verify note B is spent. LoadWalletTx invokes AddToSpends which updates mapTxSaplingNullifiers - EXPECT_TRUE(wallet.IsSaplingSpent(nullifier2)); + EXPECT_TRUE(wallet.IsSaplingSpent(nullifier2, std::nullopt)); // Verify note B belongs to wallet. EXPECT_TRUE(wallet.IsFromMe(wtx2)); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 2e3e767cc..81ce755b9 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -46,7 +46,7 @@ #include #include -#include +#include #include @@ -115,9 +115,9 @@ void ThrowIfInitialBlockDownload() } } -void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry) +void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry, const std::optional& asOfHeight) { - int confirms = wtx.GetDepthInMainChain(); + int confirms = wtx.GetDepthInMainChain(asOfHeight); std::string status = "waiting"; entry.pushKV("confirmations", confirms); @@ -264,7 +264,7 @@ UniValue getrawchangeaddress(const UniValue& params, bool fHelp) static void SendMoney(const CTxDestination &address, CAmount nValue, bool fSubtractFeeFromAmount, CWalletTx& wtxNew) { - CAmount curBalance = pwalletMain->GetBalance(); + CAmount curBalance = pwalletMain->GetBalance(std::nullopt); // Check amount if (nValue <= 0) @@ -285,7 +285,7 @@ static void SendMoney(const CTxDestination &address, CAmount nValue, bool fSubtr CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount}; vecSend.push_back(recipient); if (!pwalletMain->CreateTransaction(vecSend, wtxNew, reservekey, nFeeRequired, nChangePosRet, strError)) { - if (!fSubtractFeeFromAmount && nValue + nFeeRequired > pwalletMain->GetBalance()) + if (!fSubtractFeeFromAmount && nValue + nFeeRequired > pwalletMain->GetBalance(std::nullopt)) strError = strprintf("Error: This transaction requires a transaction fee of at least %s because of its amount, complexity, or use of recently received funds!", FormatMoney(nFeeRequired)); throw JSONRPCError(RPC_WALLET_ERROR, strError); } @@ -484,7 +484,7 @@ UniValue listaddresses(const UniValue& params, bool fHelp) // with respect to the entries in the address book for addresses generated by this wallet, // there is not a guarantee that an externally generated address (such as one associated with // a future unified incoming viewing key) will have been added to the address book. - for (const std::pair& item : pwalletMain->GetAddressBalances()) { + for (const std::pair& item : pwalletMain->GetAddressBalances(std::nullopt)) { if (t_generated_dests.count(item.first) == 0 && t_mnemonic_dests.count(item.first) == 0 && t_imported_dests.count(item.first) == 0 && @@ -841,11 +841,13 @@ UniValue listaddressgroupings(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp) + if (fHelp || params.size() > 1) throw runtime_error( - "listaddressgroupings\n" + "listaddressgroupings ( asOfHeight )\n" "\nLists groups of transparent addresses which have had their common ownership\n" "made public by common use as inputs or as the resulting change in past transactions.\n" + "\nArguments:\n" + "1. " + asOfHeightMessage(false) + "\nResult:\n" "[\n" " [\n" @@ -857,6 +859,8 @@ UniValue listaddressgroupings(const UniValue& params, bool fHelp) " ]\n" " ,...\n" "]\n" + "\nBitcoin compatibility:\n" + "The zero-argument form is compatible." "\nExamples:\n" + HelpExampleCli("listaddressgroupings", "") + HelpExampleRpc("listaddressgroupings", "") @@ -864,9 +868,11 @@ UniValue listaddressgroupings(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); + auto asOfHeight = parseAsOfHeight(params, 0); + KeyIO keyIO(Params()); UniValue jsonGroupings(UniValue::VARR); - std::map balances = pwalletMain->GetAddressBalances(); + std::map balances = pwalletMain->GetAddressBalances(asOfHeight); for (const std::set& grouping : pwalletMain->GetAddressGroupings()) { UniValue jsonGrouping(UniValue::VARR); for (const CTxDestination& address : grouping) @@ -952,16 +958,19 @@ UniValue getreceivedbyaddress(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 1 || params.size() > 3) + if (fHelp || params.size() < 1 || params.size() > 4) throw runtime_error( - "getreceivedbyaddress \"zcashaddress\" ( minconf ) ( inZat )\n" + "getreceivedbyaddress \"zcashaddress\" ( minconf inZat asOfHeight )\n" "\nReturns the total amount received by the given transparent Zcash address in transactions with at least minconf confirmations.\n" "\nArguments:\n" "1. \"zcashaddress\" (string, required) The Zcash address for transactions.\n" "2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n" "3. inZat (bool, optional, default=false) Get the result amount in " + MINOR_CURRENCY_UNIT + " (as an integer).\n" + "4. " + asOfHeightMessage(true) + "\nResult:\n" "amount (numeric) The total amount in " + CURRENCY_UNIT + "(or " + MINOR_CURRENCY_UNIT + " if inZat is true) received at this address.\n" + "\nBitcoin compatibility:\n" + "Compatible with up to two arguments." "\nExamples:\n" "\nThe amount from transactions with at least 1 confirmation\n" + HelpExampleCli("getreceivedbyaddress", "\"t14oHp2v54vfmdgQ3v3SNuQga8JKHTNi2a1\"") + @@ -987,10 +996,10 @@ UniValue getreceivedbyaddress(const UniValue& params, bool fHelp) return ValueFromAmount(0); } + auto asOfHeight = parseAsOfHeight(params, 3); + // Minimum confirmations - int nMinDepth = 1; - if (params.size() > 1) - nMinDepth = params[1].get_int(); + int nMinDepth = parseMinconf(1, params, 1, asOfHeight); // Tally CAmount nAmount = 0; @@ -1002,7 +1011,7 @@ UniValue getreceivedbyaddress(const UniValue& params, bool fHelp) for (const CTxOut& txout : wtx.vout) if (txout.scriptPubKey == scriptPubKey) - if (wtx.GetDepthInMainChain() >= nMinDepth) + if (wtx.GetDepthInMainChain(asOfHeight) >= nMinDepth) nAmount += txout.nValue; } @@ -1019,19 +1028,22 @@ UniValue getbalance(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 4) + if (fHelp || params.size() > 5) throw runtime_error( - "getbalance ( \"(dummy)\" minconf includeWatchonly inZat )\n" + "getbalance ( \"(dummy)\" minconf includeWatchonly inZat asOfHeight )\n" "\nReturns the wallet's available transparent balance. This total\n" "currently includes transparent balances associated with unified\n" "accounts. Prefer to use `z_getbalanceforaccount` instead.\n" "\nArguments:\n" "1. (dummy) (string, optional) Remains for backward compatibility. Must be excluded or set to \"*\" or \"\".\n" - "2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n" + "2. minconf (numeric, optional, default=0) Only include transactions confirmed at least this many times.\n" "3. includeWatchonly (bool, optional, default=false) Also include balance in watchonly addresses (see 'importaddress')\n" "4. inZat (bool, optional, default=false) Get the result amount in " + MINOR_CURRENCY_UNIT + " (as an integer).\n" + "5. " + asOfHeightMessage(true) + "\nResult:\n" "amount (numeric) The total amount in " + CURRENCY_UNIT + "(or " + MINOR_CURRENCY_UNIT + " if inZat is true) received.\n" + "\nBitcoin compatibility:\n" + "Compatible with up to three arguments." "\nExamples:\n" "\nThe total amount in the wallet\n" + HelpExampleCli("getbalance", "*") + @@ -1048,17 +1060,16 @@ UniValue getbalance(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_INVALID_PARAMETER, "dummy first argument must be excluded or set to \"*\" or \"\"."); } - int min_depth = 0; - if (!params[1].isNull()) { - min_depth = params[1].get_int(); - } + auto asOfHeight = parseAsOfHeight(params, 4); + + int min_depth = parseMinconf(0, params, 1, asOfHeight); isminefilter filter = ISMINE_SPENDABLE; if (!params[2].isNull() && params[2].get_bool()) { filter = filter | ISMINE_WATCH_ONLY; } - CAmount nBalance = pwalletMain->GetBalance(filter, min_depth); + CAmount nBalance = pwalletMain->GetBalance(asOfHeight, filter, min_depth); if (!params[3].isNull() && params[3].get_bool()) { return nBalance; } else { @@ -1073,12 +1084,12 @@ UniValue getunconfirmedbalance(const UniValue ¶ms, bool fHelp) if (fHelp || params.size() > 0) throw runtime_error( - "getunconfirmedbalance\n" - "Returns the server's total unconfirmed transparent balance\n"); + "getunconfirmedbalance\n" + "Returns the server's total unconfirmed transparent balance\n"); LOCK2(cs_main, pwalletMain->cs_wallet); - return ValueFromAmount(pwalletMain->GetUnconfirmedBalance()); + return ValueFromAmount(pwalletMain->GetUnconfirmedTransparentBalance()); } @@ -1134,9 +1145,7 @@ UniValue sendmany(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_INVALID_PARAMETER, "Dummy value must be set to \"\""); } UniValue sendTo = params[1].get_obj(); - int nMinDepth = 1; - if (params.size() > 2) - nMinDepth = params[2].get_int(); + int nMinDepth = parseMinconf(1, params, 2, std::nullopt); CWalletTx wtx; if (params.size() > 3 && !params[3].isNull() && !params[3].get_str().empty()) @@ -1274,10 +1283,18 @@ struct tallyitem UniValue ListReceived(const UniValue& params) { + if (params.size() > 3 && params[3].get_str() != "") { + throw JSONRPCError(RPC_INVALID_PARAMETER, "addressFilter must be set to \"\""); + } + + if (params.size() > 4 && params[4].get_bool() != false) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "includeImmatureCoinbase must be set to false"); + } + + auto asOfHeight = parseAsOfHeight(params, 5); + // Minimum confirmations - int nMinDepth = 1; - if (params.size() > 0) - nMinDepth = params[0].get_int(); + int nMinDepth = parseMinconf(1, params, 0, asOfHeight); // Whether to include empty accounts bool fIncludeEmpty = false; @@ -1297,7 +1314,7 @@ UniValue ListReceived(const UniValue& params) if (wtx.IsCoinBase() || !CheckFinalTx(wtx)) continue; - int nDepth = wtx.GetDepthInMainChain(); + int nDepth = wtx.GetDepthInMainChain(asOfHeight); if (nDepth < nMinDepth) continue; @@ -1367,9 +1384,9 @@ UniValue listreceivedbyaddress(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 3) + if (fHelp || params.size() > 6) throw runtime_error( - "listreceivedbyaddress ( minconf includeempty includeWatchonly)\n" + "listreceivedbyaddress ( minconf includeempty includeWatchonly addressFilter includeImmatureCoinbase asOfHeight )\n" "\nList balances by transparent receiving address. This API does not provide\n" "any information for associated with shielded addresses and should only be used\n" "in circumstances where it is necessary to interoperate with legacy Bitcoin\n" @@ -1378,7 +1395,9 @@ UniValue listreceivedbyaddress(const UniValue& params, bool fHelp) "1. minconf (numeric, optional, default=1) The minimum number of confirmations before payments are included.\n" "2. includeempty (numeric, optional, default=false) Whether to include addresses that haven't received any payments.\n" "3. includeWatchonly (bool, optional, default=false) Whether to include watchonly addresses (see 'importaddress').\n" - + "4. addressFilter (string, optional, default=\"\") If present and non-empty, only return information on this address. Currently, only the default value is supported.\n" + "5. includeImmatureCoinbase (bool, optional, default=false) Include immature coinbase transactions. Currently, only the default value is supported.\n" + "6. " + asOfHeightMessage(true) + "\nResult:\n" "[\n" " {\n" @@ -1390,7 +1409,8 @@ UniValue listreceivedbyaddress(const UniValue& params, bool fHelp) " }\n" " ,...\n" "]\n" - + "\nBitcoin compatibility:\n" + "Compatible up to five arguments, but can only use the default value for `addressFilter` and `includeImmatureCoinbase`." "\nExamples:\n" + HelpExampleCli("listreceivedbyaddress", "") + HelpExampleCli("listreceivedbyaddress", "6 true") @@ -1418,8 +1438,9 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest) * @param fLong Whether to include the JSON version of the transaction. * @param ret The UniValue into which the result is stored. * @param filter The "is mine" filter flags. + * @param asOfHeight The last block to look at. */ -void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter) +void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter, const std::optional& asOfHeight) { CAmount nFee; std::list listReceived; @@ -1445,14 +1466,14 @@ void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& entry.pushKV("vout", s.vout); entry.pushKV("fee", ValueFromAmount(-nFee)); if (fLong) - WalletTxToJSON(wtx, entry); + WalletTxToJSON(wtx, entry, asOfHeight); entry.pushKV("size", static_cast(GetSerializeSize(static_cast(wtx), SER_NETWORK, PROTOCOL_VERSION))); ret.push_back(entry); } } // Received - if (listReceived.size() > 0 && wtx.GetDepthInMainChain() >= nMinDepth) + if (listReceived.size() > 0 && wtx.GetDepthInMainChain(asOfHeight) >= nMinDepth) { for (const COutputEntry& r : listReceived) { @@ -1467,9 +1488,9 @@ void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& MaybePushAddress(entry, r.destination); if (wtx.IsCoinBase()) { - if (wtx.GetDepthInMainChain() < 1) + if (wtx.GetDepthInMainChain(asOfHeight) < 1) entry.pushKV("category", "orphan"); - else if (wtx.GetBlocksToMaturity() > 0) + else if (wtx.GetBlocksToMaturity(asOfHeight) > 0) entry.pushKV("category", "immature"); else entry.pushKV("category", "generate"); @@ -1482,7 +1503,7 @@ void ListTransactions(const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& entry.pushKV("amountZat", r.amount); entry.pushKV("vout", r.vout); if (fLong) - WalletTxToJSON(wtx, entry); + WalletTxToJSON(wtx, entry, asOfHeight); entry.pushKV("size", static_cast(GetSerializeSize(static_cast(wtx), SER_NETWORK, PROTOCOL_VERSION))); ret.push_back(entry); } @@ -1494,9 +1515,9 @@ UniValue listtransactions(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 4) + if (fHelp || params.size() > 5) throw runtime_error( - "listtransactions ( \"dummy\" count from includeWatchonly)\n" + "listtransactions ( \"dummy\" count from includeWatchonly asOfHeight)\n" "\nReturns up to 'count' of the most recent transactions associated with legacy transparent\n" "addresses of this wallet, skipping the first 'from' transactions.\n" "\nThis API does not provide any information about transactions containing shielded inputs\n" @@ -1508,6 +1529,7 @@ UniValue listtransactions(const UniValue& params, bool fHelp) "2. count (numeric, optional, default=10) The number of transactions to return\n" "3. from (numeric, optional, default=0) The number of transactions to skip\n" "4. includeWatchonly (bool, optional, default=false) Include transactions to watchonly addresses (see 'importaddress')\n" + "5. " + asOfHeightMessage(false) + "\nResult:\n" "[\n" " {\n" @@ -1562,6 +1584,8 @@ UniValue listtransactions(const UniValue& params, bool fHelp) if(params[3].get_bool()) filter = filter | ISMINE_WATCH_ONLY; + auto asOfHeight = parseAsOfHeight(params, 4); + if (nCount < 0) throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count"); if (nFrom < 0) @@ -1577,7 +1601,7 @@ UniValue listtransactions(const UniValue& params, bool fHelp) for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) { CWalletTx *const pwtx = (*it).second; - ListTransactions(*pwtx, 0, true, ret, filter); + ListTransactions(*pwtx, 0, true, ret, filter, asOfHeight); if ((int)ret.size() >= (nCount+nFrom)) break; } } @@ -1614,14 +1638,17 @@ UniValue listsinceblock(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp) + if (fHelp || params.size() > 6) throw runtime_error( - "listsinceblock ( \"blockhash\" target-confirmations includeWatchonly)\n" + "listsinceblock ( \"blockhash\" target-confirmations includeWatchonly includeRemoved includeChange asOfHeight )\n" "\nGet all transactions in blocks since block [blockhash], or all transactions if omitted\n" "\nArguments:\n" "1. \"blockhash\" (string, optional) The block hash to list transactions since\n" "2. target-confirmations: (numeric, optional) The confirmations required, must be 1 or more\n" "3. includeWatchonly: (bool, optional, default=false) Include transactions to watchonly addresses (see 'importaddress')" + "4. includeRemoved (bool, optional, default=true) Show transactions that were removed due to a reorg in the \"removed\" array (not guaranteed to work on pruned nodes)\n" + "5. includeChange (bool, optional, default=false) Also add entries for change outputs. Currently, only the default value is supported.\n" + "6. " + asOfHeightMessage(false) + "\nResult:\n" "{\n" " \"transactions\": [\n" @@ -1643,9 +1670,13 @@ UniValue listsinceblock(const UniValue& params, bool fHelp) " \"timereceived\": xxx, (numeric) The time received in seconds since epoch (Jan 1 1970 GMT). Available for 'send' and 'receive' category of transactions.\n" " \"comment\": \"...\", (string) If a comment is associated with the transaction.\n" " \"to\": \"...\", (string) If a comment to is associated with the transaction.\n" - " ],\n" + " ],\n" + " \"removed\": [...] (array of objects, optional) structure is the same as \"transactions\" above, only present if includeRemoved=true\n" + " Note: currently this only returns an empty array.\n" " \"lastblock\": \"lastblockhash\" (string) The hash of the last block\n" "}\n" + "\nBitcoin compatibility:\n" + "Compatible up to five arguments, but can only use the default value for `includeChange`, and only returns an empty array for \"removed\"." "\nExamples:\n" + HelpExampleCli("listsinceblock", "") + HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6") @@ -1680,6 +1711,17 @@ UniValue listsinceblock(const UniValue& params, bool fHelp) if(params[2].get_bool()) filter = filter | ISMINE_WATCH_ONLY; + bool includeRemoved = true; + if (params.size() > 3) { + includeRemoved = params[3].get_bool(); + } + + if (params.size() > 4 && params[4].get_bool() != false) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "includeChange must be set to false"); + } + + auto asOfHeight = parseAsOfHeight(params, 5); + int depth = pindex ? (1 + chainActive.Height() - pindex->nHeight) : -1; UniValue transactions(UniValue::VARR); @@ -1687,8 +1729,8 @@ UniValue listsinceblock(const UniValue& params, bool fHelp) for (const std::pair& pairWtx : pwalletMain->mapWallet) { CWalletTx tx = pairWtx.second; - if (depth == -1 || tx.GetDepthInMainChain() < depth) { - ListTransactions(tx, 0, true, transactions, filter); + if (depth == -1 || tx.GetDepthInMainChain(std::nullopt) < depth) { + ListTransactions(tx, 0, true, transactions, filter, asOfHeight); } } @@ -1697,6 +1739,9 @@ UniValue listsinceblock(const UniValue& params, bool fHelp) UniValue ret(UniValue::VOBJ); ret.pushKV("transactions", transactions); + if (includeRemoved) { + ret.pushKV("removed", UniValue(UniValue::VARR)); + } ret.pushKV("lastblock", lastblock.GetHex()); return ret; @@ -1707,15 +1752,17 @@ UniValue gettransaction(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 1 || params.size() > 2) + if (fHelp || params.size() < 1 || params.size() > 4) throw runtime_error( - "gettransaction \"txid\" ( includeWatchonly )\n" + "gettransaction \"txid\" ( includeWatchonly verbose asOfHeight )\n" "\nReturns detailed information about in-wallet transaction . This does not\n" "include complete information about shielded components of the transaction; to obtain\n" "details about shielded components of the transaction use `z_viewtransaction`.\n" "\nArguments:\n" "1. \"txid\" (string, required) The transaction id\n" - "2. \"includeWatchonly\" (bool, optional, default=false) Whether to include watchonly addresses in balance calculation and details[]\n" + "2. includeWatchonly (bool, optional, default=false) Whether to include watchonly addresses in balance calculation and details[]\n" + "3. verbose (bool, optional, default=false) Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction). Currently, only the default value is supported.\n" + "4. " + asOfHeightMessage(false) + "\nResult:\n" "{\n" " \"status\" : \"mined|waiting|expiringsoon|expired\", (string) The transaction status, can be 'mined', 'waiting', 'expiringsoon' or 'expired'\n" @@ -1752,7 +1799,8 @@ UniValue gettransaction(const UniValue& params, bool fHelp) " ],\n" " \"hex\" : \"data\" (string) Raw data for transaction\n" "}\n" - + "\nBitcoin compatibility:\n" + "Compatible up to three arguments, but can only use the default value for `verbose`." "\nExamples:\n" + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true") @@ -1769,12 +1817,18 @@ UniValue gettransaction(const UniValue& params, bool fHelp) if(params[1].get_bool()) filter = filter | ISMINE_WATCH_ONLY; + if (params.size() > 2 && params[2].get_bool() != false) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "verbose must be set to false"); + } + + auto asOfHeight = parseAsOfHeight(params, 3); + UniValue entry(UniValue::VOBJ); if (!pwalletMain->mapWallet.count(hash)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); const CWalletTx& wtx = pwalletMain->mapWallet[hash]; - CAmount nCredit = wtx.GetCredit(filter); + CAmount nCredit = wtx.GetCredit(asOfHeight, filter); CAmount nDebit = wtx.GetDebit(filter); CAmount nNet = nCredit - nDebit; CAmount nFee = (wtx.IsFromMe(filter) ? wtx.GetValueOut() - nDebit : 0); @@ -1785,10 +1839,10 @@ UniValue gettransaction(const UniValue& params, bool fHelp) if (wtx.IsFromMe(filter)) entry.pushKV("fee", ValueFromAmount(nFee)); - WalletTxToJSON(wtx, entry); + WalletTxToJSON(wtx, entry, asOfHeight); UniValue details(UniValue::VARR); - ListTransactions(wtx, 0, false, details, filter); + ListTransactions(wtx, 0, false, details, filter, asOfHeight); entry.pushKV("details", details); string strHex = EncodeHexTx(static_cast(wtx)); @@ -2303,25 +2357,29 @@ UniValue settxfee(const UniValue& params, bool fHelp) return true; } -CAmount getBalanceZaddr(std::optional address, int minDepth = 1, int maxDepth = INT_MAX, bool ignoreUnspendable=true); +CAmount getBalanceZaddr(std::optional address, const std::optional& asOfHeight, int minDepth = 1, int maxDepth = INT_MAX, bool ignoreUnspendable=true); UniValue getwalletinfo(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() != 0) + if (fHelp || params.size() > 1) throw runtime_error( - "getwalletinfo\n" + "getwalletinfo ( asOfHeight )\n" "Returns wallet state information.\n" + "\nArguments:\n" + "1. " + asOfHeightMessage(false) + "\nResult:\n" "{\n" " \"walletversion\": xxxxx, (numeric) the wallet version\n" " \"balance\": xxxxxxx, (numeric) the total confirmed transparent balance of the wallet in " + CURRENCY_UNIT + "\n" - " \"unconfirmed_balance\": xxx, (numeric) the total unconfirmed transparent balance of the wallet in " + CURRENCY_UNIT + "\n" + " \"unconfirmed_balance\": xxx, (numeric, optional) the total unconfirmed transparent balance of the wallet in " + CURRENCY_UNIT + ".\n" + " Not included if `asOfHeight` is specified.\n" " \"immature_balance\": xxxxxx, (numeric) the total immature transparent balance of the wallet in " + CURRENCY_UNIT + "\n" " \"shielded_balance\": xxxxxxx, (numeric) the total confirmed shielded balance of the wallet in " + CURRENCY_UNIT + "\n" - " \"shielded_unconfirmed_balance\": xxx, (numeric) the total unconfirmed shielded balance of the wallet in " + CURRENCY_UNIT + "\n" + " \"shielded_unconfirmed_balance\": xxx, (numeric, optional) the total unconfirmed shielded balance of the wallet in " + CURRENCY_UNIT + ".\n" + " Not included if `asOfHeight` is specified.\n" " \"txcount\": xxxxxxx, (numeric) the total number of transactions in the wallet\n" " \"keypoololdest\": xxxxxx, (numeric) the timestamp (seconds since GMT epoch) of the oldest pre-generated key in the key pool\n" " \"keypoolsize\": xxxx, (numeric) how many new keys are pre-generated\n" @@ -2337,15 +2395,21 @@ UniValue getwalletinfo(const UniValue& params, bool fHelp) + HelpExampleRpc("getwalletinfo", "") ); + auto asOfHeight = parseAsOfHeight(params, 0); + LOCK2(cs_main, pwalletMain->cs_wallet); UniValue obj(UniValue::VOBJ); obj.pushKV("walletversion", pwalletMain->GetVersion()); - obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance())); - obj.pushKV("unconfirmed_balance", ValueFromAmount(pwalletMain->GetUnconfirmedBalance())); - obj.pushKV("immature_balance", ValueFromAmount(pwalletMain->GetImmatureBalance())); - obj.pushKV("shielded_balance", FormatMoney(getBalanceZaddr(std::nullopt, 1, INT_MAX))); - obj.pushKV("shielded_unconfirmed_balance", FormatMoney(getBalanceZaddr(std::nullopt, 0, 0))); + obj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance(asOfHeight))); + if (!asOfHeight.has_value()) { + obj.pushKV("unconfirmed_balance", ValueFromAmount(pwalletMain->GetUnconfirmedTransparentBalance())); + } + obj.pushKV("immature_balance", ValueFromAmount(pwalletMain->GetImmatureBalance(asOfHeight))); + obj.pushKV("shielded_balance", FormatMoney(getBalanceZaddr(std::nullopt, asOfHeight, 1, INT_MAX))); + if (!asOfHeight.has_value()) { + obj.pushKV("shielded_unconfirmed_balance", FormatMoney(getBalanceZaddr(std::nullopt, asOfHeight, 0, 0))); + } obj.pushKV("txcount", (int)pwalletMain->mapWallet.size()); obj.pushKV("keypoololdest", pwalletMain->GetOldestKeyPoolTime()); obj.pushKV("keypoolsize", (int)pwalletMain->GetKeyPoolSize()); @@ -2394,9 +2458,9 @@ UniValue listunspent(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 3) + if (fHelp || params.size() > 6) throw runtime_error( - "listunspent ( minconf maxconf [\"address\",...] )\n" + "listunspent ( minconf maxconf [\"address\",...] includeUnsafe queryOptions asOfHeight )\n" "\nReturns array of unspent transparent transaction outputs with between minconf and\n" "maxconf (inclusive) confirmations. Use `z_listunspent` instead to see information\n" "related to unspent shielded notes. Results may be optionally filtered to only include\n" @@ -2409,6 +2473,9 @@ UniValue listunspent(const UniValue& params, bool fHelp) " \"address\" (string) Zcash address\n" " ,...\n" " ]\n" + "4. includeUnsafe (bool, optional, default=true) Include outputs that are not safe to spend. Currently, only the default value is supported.\n" + "5. queryOptions (object, optional, default={}) JSON with query options. Currently, only the default value is supported.\n" + "6. " + asOfHeightMessage(true) + "\nResult\n" "[ (array of json object)\n" " {\n" @@ -2425,7 +2492,8 @@ UniValue listunspent(const UniValue& params, bool fHelp) " }\n" " ,...\n" "]\n" - + "\nBitcoin compatibility:\n" + "Compatible up to five arguments, but can only use the default value for `includeUnsafe` and `queryOptions`." "\nExamples\n" + HelpExampleCli("listunspent", "") + HelpExampleCli("listunspent", "6 9999999 \"[\\\"t1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\",\\\"t1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"") @@ -2434,9 +2502,9 @@ UniValue listunspent(const UniValue& params, bool fHelp) RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VARR)); - int nMinDepth = 1; - if (params.size() > 0) - nMinDepth = params[0].get_int(); + auto asOfHeight = parseAsOfHeight(params, 5); + + int nMinDepth = parseMinconf(1, params, 0, asOfHeight); int nMaxDepth = 9999999; if (params.size() > 1) @@ -2458,10 +2526,28 @@ UniValue listunspent(const UniValue& params, bool fHelp) } } + if (params.size() > 3 && params[3].get_bool() != false) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "includeUnsafe must be set to false"); + } + + if (params.size() > 4 && !params[4].get_obj().empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "queryOptions must be set to {}"); + } + + LOCK2(cs_main, pwalletMain->cs_wallet); + UniValue results(UniValue::VARR); vector vecOutputs; - LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->AvailableCoins(vecOutputs, false, NULL, true); + pwalletMain->AvailableCoins( + vecOutputs, + asOfHeight, + false, // fOnlyConfirmed + nullptr, // coinControl + true, // fIncludeZeroValue + true, // fIncludeCoinBase + false, // fOnlySpendable + nMinDepth, + destinations); for (const COutput& out : vecOutputs) { if (out.nDepth < nMinDepth || out.nDepth > nMaxDepth) continue; @@ -2470,9 +2556,6 @@ UniValue listunspent(const UniValue& params, bool fHelp) const CScript& scriptPubKey = out.tx->vout[out.i].scriptPubKey; bool fValidAddress = ExtractDestination(scriptPubKey, address); - if (destinations.size() && (!fValidAddress || !destinations.count(address))) - continue; - UniValue entry(UniValue::VOBJ); entry.pushKV("txid", out.tx->GetHash().GetHex()); entry.pushKV("vout", out.i); @@ -2506,9 +2589,9 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 4) + if (fHelp || params.size() > 5) throw runtime_error( - "z_listunspent ( minconf maxconf includeWatchonly [\"zaddr\",...] )\n" + "z_listunspent ( minconf maxconf includeWatchonly [\"zaddr\",...] asOfHeight )\n" "\nReturns an array of unspent shielded notes with between minconf and maxconf (inclusive)\n" "confirmations. Results may be optionally filtered to only include notes sent to specified\n" "addresses.\n" @@ -2523,6 +2606,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) " \"address\" (string) Sprout, Sapling, or Unified address\n" " ,...\n" " ]\n" + "5. " + asOfHeightMessage(true) + "\nResult (output indices for only one value pool will be present):\n" "[ (array of json object)\n" " {\n" @@ -2550,13 +2634,9 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VBOOL)(UniValue::VARR)); - int nMinDepth = 1; - if (params.size() > 0) { - nMinDepth = params[0].get_int(); - } - if (nMinDepth < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } + auto asOfHeight = parseAsOfHeight(params, 4); + + int nMinDepth = parseMinconf(1, params, 0, asOfHeight); int nMaxDepth = 9999999; if (params.size() > 1) { @@ -2626,7 +2706,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) std::vector sproutEntries; std::vector saplingEntries; std::vector orchardEntries; - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, asOfHeight, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false); for (auto & entry : sproutEntries) { UniValue obj(UniValue::VOBJ); @@ -3759,13 +3839,13 @@ UniValue z_listunifiedreceivers(const UniValue& params, bool fHelp) return result; } -CAmount getBalanceTaddr(const std::optional& taddr, int minDepth=1, bool ignoreUnspendable=true) { +CAmount getBalanceTaddr(const std::optional& taddr, const std::optional& asOfHeight, int minDepth=1, bool ignoreUnspendable=true) { vector vecOutputs; CAmount balance = 0; LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->AvailableCoins(vecOutputs, false, NULL, true); + pwalletMain->AvailableCoins(vecOutputs, asOfHeight, false, NULL, true); for (const COutput& out : vecOutputs) { if (out.nDepth < minDepth) { continue; @@ -3792,7 +3872,7 @@ CAmount getBalanceTaddr(const std::optional& taddr, int minDepth return balance; } -CAmount getBalanceZaddr(std::optional address, int minDepth, int maxDepth, bool ignoreUnspendable) { +CAmount getBalanceZaddr(std::optional address, const std::optional& asOfHeight, int minDepth, int maxDepth, bool ignoreUnspendable) { CAmount balance = 0; std::vector sproutEntries; std::vector saplingEntries; @@ -3804,7 +3884,7 @@ CAmount getBalanceZaddr(std::optional address, int min noteFilter = NoteFilter::ForPaymentAddresses(std::vector({address.value()})); } - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, minDepth, maxDepth, true, ignoreUnspendable); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, asOfHeight, minDepth, maxDepth, true, ignoreUnspendable); for (auto & entry : sproutEntries) { balance += CAmount(entry.note.value()); } @@ -3840,13 +3920,14 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size()==0 || params.size() >2) + if (fHelp || params.size() == 0 || params.size() > 3) throw runtime_error( - "z_listreceivedbyaddress \"address\" ( minconf )\n" + "z_listreceivedbyaddress \"address\" ( minconf asOfHeight )\n" "\nReturn a list of amounts received by a zaddr belonging to the node's wallet.\n" "\nArguments:\n" "1. \"address\" (string) The shielded address.\n" "2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n" + "3. " + asOfHeightMessage(true) + "\nResult (output indices for only one value pool will be present):\n" "[\n" " {\n" @@ -3873,13 +3954,10 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); - int nMinDepth = 1; - if (params.size() > 1) { - nMinDepth = params[1].get_int(); - } - if (nMinDepth < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } + auto asOfHeight = parseAsOfHeight(params, 2); + + int nMinDepth = parseMinconf(1, params, 1, asOfHeight); + UniValue result(UniValue::VARR); // Check that the from address is valid. @@ -3925,7 +4003,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) std::vector orchardEntries; auto noteFilter = NoteFilter::ForPaymentAddresses(std::vector({decoded.value()})); - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, INT_MAX, false, false); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, asOfHeight, nMinDepth, INT_MAX, false, false); auto push_transparent_result = [&](const CTxDestination& dest) -> void { const CScript scriptPubKey{GetScriptForDestination(dest)}; @@ -3933,7 +4011,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) if (!CheckFinalTx(wtx)) continue; - int nDepth = wtx.GetDepthInMainChain(); + int nDepth = wtx.GetDepthInMainChain(asOfHeight); if (nDepth < nMinDepth) continue; for (size_t i = 0; i < wtx.vout.size(); ++i) { const CTxOut& txout{wtx.vout[i]}; @@ -4115,13 +4193,7 @@ UniValue z_getbalance(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); - int nMinDepth = 1; - if (params.size() > 1) { - nMinDepth = params[1].get_int(); - } - if (nMinDepth < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } + int nMinDepth = parseMinconf(1, params, 1, std::nullopt); KeyIO keyIO(Params()); // Check that the from address is valid. @@ -4138,16 +4210,16 @@ UniValue z_getbalance(const UniValue& params, bool fHelp) CAmount nBalance = 0; std::visit(match { [&](const CKeyID& addr) { - nBalance = getBalanceTaddr(addr, nMinDepth, false); + nBalance = getBalanceTaddr(addr, std::nullopt, nMinDepth, false); }, [&](const CScriptID& addr) { - nBalance = getBalanceTaddr(addr, nMinDepth, false); + nBalance = getBalanceTaddr(addr, std::nullopt, nMinDepth, false); }, [&](const libzcash::SproutPaymentAddress& addr) { - nBalance = getBalanceZaddr(addr, nMinDepth, INT_MAX, false); + nBalance = getBalanceZaddr(addr, std::nullopt, nMinDepth, INT_MAX, false); }, [&](const libzcash::SaplingPaymentAddress& addr) { - nBalance = getBalanceZaddr(addr, nMinDepth, INT_MAX, false); + nBalance = getBalanceZaddr(addr, std::nullopt, nMinDepth, INT_MAX, false); }, [&](const libzcash::UnifiedAddress& addr) { auto selector = pwalletMain->ZTXOSelectorForAddress(addr, true, false); @@ -4156,7 +4228,7 @@ UniValue z_getbalance(const UniValue& params, bool fHelp) RPC_INVALID_ADDRESS_OR_KEY, "Unified address does not correspond to an account in the wallet"); } - auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, nMinDepth); + auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, nMinDepth, std::nullopt); for (const auto& t : spendableInputs.utxos) { nBalance += t.Value(); @@ -4183,15 +4255,16 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 1 || params.size() > 2) + if (fHelp || params.size() < 1 || params.size() > 3) throw runtime_error( - "z_getbalanceforviewingkey \"fvk\" ( minconf )\n" + "z_getbalanceforviewingkey \"fvk\" ( minconf asOfHeight )\n" "\nReturns the balance viewable by a full viewing key known to the node's wallet" "\nfor each value pool. Sprout viewing keys may be used only if the wallet controls" "\nthe corresponding spending key." "\nArguments:\n" "1. \"fvk\" (string) The selected full viewing key.\n" "2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n" + "3. " + asOfHeightMessage(true) + "\nResult:\n" "{\n" " \"pools\": {\n" @@ -4228,13 +4301,9 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp) } auto fvk = decoded.value(); - int minconf = 1; - if (params.size() > 1) { - minconf = params[1].get_int(); - if (minconf < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } - } + auto asOfHeight = parseAsOfHeight(params, 2); + + int minconf = parseMinconf(1, params, 1, asOfHeight); LOCK2(cs_main, pwalletMain->cs_wallet); @@ -4251,7 +4320,7 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp) "Error: the wallet does not recognize the specified viewing key."); } - auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf); + auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf, asOfHeight); CAmount transparentBalance = 0; CAmount sproutBalance = 0; @@ -4295,13 +4364,14 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 1 || params.size() > 2) + if (fHelp || params.size() < 1 || params.size() > 3) throw runtime_error( - "z_getbalanceforaccount account ( minconf )\n" + "z_getbalanceforaccount account ( minconf asOfHeight )\n" "\nReturns the account's spendable balance for each value pool (\"transparent\", \"sapling\", and \"orchard\")." "\nArguments:\n" "1. account (numeric) The account number.\n" "2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n" + "3. " + asOfHeightMessage(true) + "\nResult:\n" "{\n" " \"pools\": {\n" @@ -4334,13 +4404,9 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp) } libzcash::AccountId account = accountInt; - int minconf = 1; - if (params.size() > 1) { - minconf = params[1].get_int(); - if (minconf < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } - } + auto asOfHeight = parseAsOfHeight(params, 2); + + int minconf = parseMinconf(1, params, 1, asOfHeight); LOCK2(cs_main, pwalletMain->cs_wallet); @@ -4352,7 +4418,7 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp) tfm::format("Error: account %d has not been generated by z_getnewaccount.", account)); } - auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf); + auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf, asOfHeight); // Accounts never contain Sprout notes. assert(spendableInputs.sproutNoteEntries.empty()); @@ -4428,13 +4494,7 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); - int nMinDepth = 1; - if (params.size() > 0) { - nMinDepth = params[0].get_int(); - } - if (nMinDepth < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } + int nMinDepth = parseMinconf(1, params, 0, std::nullopt); bool fIncludeWatchonly = false; if (params.size() > 1) { @@ -4445,8 +4505,8 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp) // but they don't because wtx.GetAmounts() does not handle tx where there are no outputs // pwalletMain->GetBalance() does not accept min depth parameter // so we use our own method to get balance of utxos. - CAmount nBalance = getBalanceTaddr(std::nullopt, nMinDepth, !fIncludeWatchonly); - CAmount nPrivateBalance = getBalanceZaddr(std::nullopt, nMinDepth, INT_MAX, !fIncludeWatchonly); + CAmount nBalance = getBalanceTaddr(std::nullopt, std::nullopt, nMinDepth, !fIncludeWatchonly); + CAmount nPrivateBalance = getBalanceZaddr(std::nullopt, std::nullopt, nMinDepth, INT_MAX, !fIncludeWatchonly); CAmount nTotalBalance = nBalance + nPrivateBalance; UniValue result(UniValue::VOBJ); result.pushKV("transparent", FormatMoney(nBalance)); @@ -5004,11 +5064,11 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) if (fHelp || params.size() < 2 || params.size() > 5) throw runtime_error( "z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) ( privacyPolicy )\n" - "\nSend multiple times. Amounts are decimal numbers with at most 8 digits of precision." - "\nChange generated from one or more transparent addresses flows to a new transparent" - "\naddress, while change generated from a legacy Sapling address returns to itself." - "\nWhen sending from a unified address, change is returned to the internal-only address" - "\nfor the associated unified account." + "\nSend a transaction with multiple recipients. Amounts are decimal numbers with at" + "\nmost 8 digits of precision. Change generated from one or more transparent" + "\naddresses flows to a new transparent address, while change generated from a" + "\nlegacy Sapling address returns to itself. When sending from a unified address," + "\nchange is returned to the internal-only address for the associated unified account." "\nWhen spending coinbase UTXOs, only shielded recipients are permitted and change is not allowed;" "\nthe entire value of the coinbase UTXO(s) must be consumed." + HelpRequiringPassphrase() + "\n" @@ -5258,13 +5318,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) } // Minimum confirmations - int nMinDepth = DEFAULT_NOTE_CONFIRMATIONS; - if (params.size() > 2) { - nMinDepth = params[2].get_int(); - } - if (nMinDepth < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); - } + int nMinDepth = parseMinconf(DEFAULT_NOTE_CONFIRMATIONS, params, 2, std::nullopt); // Fee in Zatoshis, not currency format) CAmount nFee = DEFAULT_FEE; @@ -5354,13 +5408,15 @@ UniValue z_setmigration(const UniValue& params, bool fHelp) { UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() != 0) + if (fHelp || params.size() > 1) throw runtime_error( - "z_getmigrationstatus\n" + "z_getmigrationstatus ( asOfHeight )\n" "Returns information about the status of the Sprout to Sapling migration.\n" "Note: A transaction is defined as finalized if it has at least ten confirmations.\n" "Also, it is possible that manually created transactions involving this wallet\n" "will be included in the result.\n" + "\nArguments:\n" + "1. " + asOfHeightMessage(false) + "\nResult:\n" "{\n" " \"enabled\": true|false, (boolean) Whether or not migration is enabled\n" @@ -5374,6 +5430,9 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) { "}\n" ); LOCK2(cs_main, pwalletMain->cs_wallet); + + auto asOfHeight = parseAsOfHeight(params, 0); + UniValue migrationStatus(UniValue::VOBJ); migrationStatus.pushKV("enabled", pwalletMain->fSaplingMigrationEnabled); // The "destination_address" field MAY be omitted if the "-migrationdestaddress" @@ -5391,7 +5450,7 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) { std::vector orchardEntries; // Here we are looking for any and all Sprout notes for which we have the spending key, including those // which are locked and/or only exist in the mempool, as they should be included in the unmigrated amount. - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, true, true, false); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, asOfHeight, 0, INT_MAX, true, true, false); CAmount unmigratedAmount = 0; for (const auto& sproutEntry : sproutEntries) { unmigratedAmount += sproutEntry.note.value(); @@ -5426,7 +5485,7 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) { migrationTxids.push_back(txPair.first.ToString()); // A transaction is "finalized" iff it has at least 10 confirmations. // TODO: subject to change, if the recommended number of confirmations changes. - if (tx.GetDepthInMainChain() >= 10) { + if (tx.GetDepthInMainChain(asOfHeight) >= 10) { finalizedMigratedAmount -= tx.GetValueBalanceSapling(); ++numFinalizedMigrationTxs; } else { @@ -5603,7 +5662,7 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp) // Get available utxos vector vecOutputs; - pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, true); + pwalletMain->AvailableCoins(vecOutputs, std::nullopt, true, NULL, false, true); // Find unspent coinbase utxos and update estimated size for (const COutput& out : vecOutputs) { @@ -5965,7 +6024,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) if (useAnyUTXO || taddrs.size() > 0) { // Get available utxos vector vecOutputs; - pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, false); + pwalletMain->AvailableCoins(vecOutputs, std::nullopt, true, NULL, false, false); // Find unspent utxos and update estimated size for (const COutput& out : vecOutputs) { @@ -6016,7 +6075,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) useAnySprout || useAnySapling ? std::nullopt : std::optional(NoteFilter::ForPaymentAddresses(zaddrs)); - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nAnchorConfirmations); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, std::nullopt, nAnchorConfirmations); // If Sapling is not active, do not allow sending from a sapling addresses. if (!saplingActive && saplingEntries.size() > 0) { @@ -6241,12 +6300,13 @@ UniValue z_getnotescount(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 1) + if (fHelp || params.size() > 2) throw runtime_error( - "z_getnotescount\n" + "z_getnotescount ( minconf asOfHeight )\n" + "\nReturns the number of notes available in the wallet for each shielded value pool.\n" "\nArguments:\n" "1. minconf (numeric, optional, default=1) Only include notes in transactions confirmed at least this many times.\n" - "\nReturns the number of notes available in the wallet for each shielded value pool.\n" + "2. " + asOfHeightMessage(true) + "\nResult:\n" "{\n" " \"sprout\" (numeric) the number of Sprout notes in the wallet\n" @@ -6260,15 +6320,15 @@ UniValue z_getnotescount(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); - int nMinDepth = 1; - if (params.size() > 0) - nMinDepth = params[0].get_int(); + auto asOfHeight = parseAsOfHeight(params, 1); + + int nMinDepth = parseMinconf(1, params, 0, asOfHeight); int sprout = 0; int sapling = 0; int orchard = 0; for (auto& wtx : pwalletMain->mapWallet) { - if (wtx.second.GetDepthInMainChain() >= nMinDepth) { + if (wtx.second.GetDepthInMainChain(asOfHeight) >= nMinDepth) { sprout += wtx.second.mapSproutNoteData.size(); sapling += wtx.second.mapSaplingNoteData.size(); orchard += wtx.second.orchardTxMeta.GetMyActionIVKs().size(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 00a738489..898cf37dc 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -876,7 +876,7 @@ std::pair CWallet::GetPaymentAddressForRecipient( auto wtxPtr = mapWallet.find(txid); if (wtxPtr != mapWallet.end()) { const CBlockIndex* pTxIndex{nullptr}; - if (wtxPtr->second.GetDepthInMainChain(pTxIndex) > 0) { + if (wtxPtr->second.GetDepthInMainChain(pTxIndex, std::nullopt) > 0) { nHeight = pTxIndex->nHeight; } } @@ -2206,7 +2206,8 @@ std::optional CWallet::GenerateChangeAddressForAccount( SpendableInputs CWallet::FindSpendableInputs( ZTXOSelector selector, bool allowTransparentCoinbase, - uint32_t minDepth) const { + uint32_t minDepth, + const std::optional& asOfHeight) const { AssertLockHeld(cs_main); AssertLockHeld(cs_wallet); @@ -2220,7 +2221,7 @@ SpendableInputs CWallet::FindSpendableInputs( SpendableInputs unspent; for (auto const& [wtxid, wtx] : mapWallet) { bool isCoinbase = wtx.IsCoinBase(); - auto nDepth = wtx.GetDepthInMainChain(); + auto nDepth = wtx.GetDepthInMainChain(asOfHeight); // Filter the transactions before checking for coins if (!CheckFinalTx(wtx)) continue; @@ -2228,14 +2229,14 @@ SpendableInputs CWallet::FindSpendableInputs( if (selectTransparent && // skip transparent utxo selection if coinbase spend restrictions are not met - (!isCoinbase || (allowTransparentCoinbase && wtx.GetBlocksToMaturity() <= 0))) { + (!isCoinbase || (allowTransparentCoinbase && wtx.GetBlocksToMaturity(asOfHeight) <= 0))) { for (int i = 0; i < wtx.vout.size(); i++) { const auto& output = wtx.vout[i]; isminetype mine = IsMine(output); // skip spent utxos - if (IsSpent(wtxid, i)) continue; + if (IsSpent(wtxid, i, asOfHeight)) continue; // skip utxos that don't belong to the wallet if (mine == ISMINE_NO) continue; // skip utxos that for which we don't have the spending keys, if @@ -2263,7 +2264,7 @@ SpendableInputs CWallet::FindSpendableInputs( SproutPaymentAddress pa = nd.address; // skip note which has been spent - if (nd.nullifier.has_value() && IsSproutSpent(nd.nullifier.value())) continue; + if (nd.nullifier.has_value() && IsSproutSpent(nd.nullifier.value(), asOfHeight)) continue; // skip notes which don't match the source if (!this->SelectorMatchesAddress(selector, pa)) continue; // skip notes for which we don't have the spending key @@ -2297,7 +2298,7 @@ SpendableInputs CWallet::FindSpendableInputs( (unsigned char) j); unspent.sproutNoteEntries.push_back(SproutNoteEntry { - jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain() }); + jsop, pa, plaintext.note(pa), plaintext.memo(), nDepth }); } catch (const note_decryption_failed &err) { // Couldn't decrypt with this spending key @@ -2327,7 +2328,7 @@ SpendableInputs CWallet::FindSpendableInputs( auto pa = maybe_pa.value(); // skip notes which have been spent - if (nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value())) continue; + if (nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value(), asOfHeight)) continue; // skip notes which do not match the source if (!this->SelectorMatchesAddress(selector, pa)) continue; // skip notes if we don't have the spending key @@ -2337,7 +2338,7 @@ SpendableInputs CWallet::FindSpendableInputs( auto note = notePt.note(nd.ivk).value(); unspent.saplingNoteEntries.push_back(SaplingNoteEntry { - op, pa, note, notePt.memo(), wtx.GetDepthInMainChain() }); + op, pa, note, notePt.memo(), nDepth }); } } } @@ -2386,7 +2387,7 @@ SpendableInputs CWallet::FindSpendableInputs( orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true); for (auto& noteMeta : incomingNotes) { - if (IsOrchardSpent(noteMeta.GetOutPoint())) { + if (IsOrchardSpent(noteMeta.GetOutPoint(), asOfHeight)) { continue; } @@ -2396,7 +2397,7 @@ SpendableInputs CWallet::FindSpendableInputs( // the transaction does not exist in the main wallet. assert(mit != mapWallet.end()); - int confirmations = mit->second.GetDepthInMainChain(); + int confirmations = mit->second.GetDepthInMainChain(asOfHeight); if (confirmations < 0) continue; if (confirmations >= minDepth) { noteMeta.SetConfirmations(confirmations); @@ -2413,7 +2414,7 @@ SpendableInputs CWallet::FindSpendableInputs( * Outpoint is spent if any non-conflicted transaction * spends it: */ -bool CWallet::IsSpent(const uint256& hash, unsigned int n) const +bool CWallet::IsSpent(const uint256& hash, unsigned int n, const std::optional& asOfHeight) const { const COutPoint outpoint(hash, n); pair range; @@ -2423,8 +2424,9 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const { const uint256& wtxid = it->second; std::map::const_iterator mit = mapWallet.find(wtxid); - if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) + if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) { return true; // Spent + } } return false; } @@ -2433,7 +2435,7 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const * Note is spent if any non-conflicted transaction * spends it: */ -bool CWallet::IsSproutSpent(const uint256& nullifier) const { +bool CWallet::IsSproutSpent(const uint256& nullifier, const std::optional& asOfHeight) const { LOCK(cs_main); pair range; range = mapTxSproutNullifiers.equal_range(nullifier); @@ -2441,14 +2443,14 @@ bool CWallet::IsSproutSpent(const uint256& nullifier) const { for (TxNullifiers::const_iterator it = range.first; it != range.second; ++it) { const uint256& wtxid = it->second; std::map::const_iterator mit = mapWallet.find(wtxid); - if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) { + if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) { return true; // Spent } } return false; } -bool CWallet::IsSaplingSpent(const uint256& nullifier) const { +bool CWallet::IsSaplingSpent(const uint256& nullifier, const std::optional& asOfHeight) const { LOCK(cs_main); pair range; range = mapTxSaplingNullifiers.equal_range(nullifier); @@ -2456,17 +2458,17 @@ bool CWallet::IsSaplingSpent(const uint256& nullifier) const { for (TxNullifiers::const_iterator it = range.first; it != range.second; ++it) { const uint256& wtxid = it->second; std::map::const_iterator mit = mapWallet.find(wtxid); - if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) { + if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) { return true; // Spent } } return false; } -bool CWallet::IsOrchardSpent(const OrchardOutPoint& outpoint) const { +bool CWallet::IsOrchardSpent(const OrchardOutPoint& outpoint, const std::optional& asOfHeight) const { for (const auto& txid : orchardWallet.GetPotentialSpends(outpoint)) { std::map::const_iterator mit = mapWallet.find(txid); - if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) { + if (mit != mapWallet.end() && mit->second.GetDepthInMainChain(asOfHeight) >= 0) { return true; // Spent } } @@ -4834,7 +4836,7 @@ void CWallet::ReacceptWalletTransactions() CWalletTx& wtx = item.second; assert(wtx.GetHash() == wtxid); - int nDepth = wtx.GetDepthInMainChain(); + int nDepth = wtx.GetDepthInMainChain(std::nullopt); if (!wtx.IsCoinBase() && nDepth < 0) { mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx)); @@ -4854,7 +4856,7 @@ bool CWalletTx::RelayWalletTransaction() assert(pwallet->GetBroadcastTransactions()); if (!IsCoinBase()) { - if (GetDepthInMainChain() == 0) { + if (GetDepthInMainChain(std::nullopt) == 0) { LogPrintf("Relaying wtx %s\n", GetHash().ToString()); RelayTransaction((CTransaction)*this); return true; @@ -4906,10 +4908,10 @@ CAmount CWalletTx::GetDebit(const isminefilter& filter) const return debit; } -CAmount CWalletTx::GetCredit(const isminefilter& filter) const +CAmount CWalletTx::GetCredit(const std::optional& asOfHeight, const isminefilter& filter) const { // Must wait until coinbase is safely deep enough in the chain before valuing it - if (IsCoinBase() && GetBlocksToMaturity() > 0) + if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0) return 0; int64_t credit = 0; @@ -4939,9 +4941,9 @@ CAmount CWalletTx::GetCredit(const isminefilter& filter) const return credit; } -CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const +CAmount CWalletTx::GetImmatureCredit(const std::optional& asOfHeight, bool fUseCache) const { - if (IsCoinBase() && GetBlocksToMaturity() > 0 && IsInMainChain()) + if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0 && IsInMainChain(asOfHeight)) { if (fUseCache && fImmatureCreditCached) return nImmatureCreditCached; @@ -4953,13 +4955,13 @@ CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const return 0; } -CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const +CAmount CWalletTx::GetAvailableCredit(const std::optional& asOfHeight, bool fUseCache, const isminefilter& filter) const { if (pwallet == nullptr) return 0; // Must wait until coinbase is safely deep enough in the chain before valuing it - if (IsCoinBase() && GetBlocksToMaturity() > 0) + if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0) return 0; CAmount* cache = nullptr; @@ -4981,7 +4983,7 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter uint256 hashTx = GetHash(); for (unsigned int i = 0; i < vout.size(); i++) { - if (!pwallet->IsSpent(hashTx, i)) + if (!pwallet->IsSpent(hashTx, i, asOfHeight)) { const CTxOut &txout = vout[i]; nCredit += pwallet->GetCredit(txout, filter); @@ -4997,9 +4999,9 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter return nCredit; } -CAmount CWalletTx::GetImmatureWatchOnlyCredit(const bool fUseCache) const +CAmount CWalletTx::GetImmatureWatchOnlyCredit(const std::optional& asOfHeight, const bool fUseCache) const { - if (IsCoinBase() && GetBlocksToMaturity() > 0 && IsInMainChain()) + if (IsCoinBase() && GetBlocksToMaturity(asOfHeight) > 0 && IsInMainChain(asOfHeight)) { if (fUseCache && fImmatureWatchCreditCached) return nImmatureWatchCreditCached; @@ -5040,14 +5042,17 @@ bool CWalletTx::IsFromMe(const isminefilter& filter) const return false; } -bool CWalletTx::IsTrusted() const +bool CWalletTx::IsTrusted(const std::optional& asOfHeight) const { // Quick answer in most cases if (!CheckFinalTx(*this)) return false; - int nDepth = GetDepthInMainChain(); + int nDepth = GetDepthInMainChain(asOfHeight); if (nDepth >= 1) return true; + if (asOfHeight.has_value() && nDepth == 0) + // don’t trust mempool tx if using `asOfHeight` + return false; if (nDepth < 0) return false; if (!bSpendZeroConfChange || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit @@ -5125,7 +5130,7 @@ void CWallet::ResendWalletTransactions(int64_t nBestBlockTime) */ -CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) const +CAmount CWallet::GetBalance(const std::optional& asOfHeight, const isminefilter& filter, const int min_depth) const { CAmount nTotal = 0; { @@ -5133,8 +5138,8 @@ CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) con for (const auto& entry : mapWallet) { const CWalletTx* pcoin = &entry.second; - if (pcoin->IsTrusted() && pcoin->GetDepthInMainChain() >= min_depth) { - nTotal += pcoin->GetAvailableCredit(true, filter); + if (pcoin->IsTrusted(asOfHeight) && pcoin->GetDepthInMainChain(asOfHeight) >= min_depth) { + nTotal += pcoin->GetAvailableCredit(asOfHeight, true, filter); } } } @@ -5142,7 +5147,7 @@ CAmount CWallet::GetBalance(const isminefilter& filter, const int min_depth) con return nTotal; } -CAmount CWallet::GetUnconfirmedBalance() const +CAmount CWallet::GetUnconfirmedTransparentBalance() const { CAmount nTotal = 0; { @@ -5150,14 +5155,14 @@ CAmount CWallet::GetUnconfirmedBalance() const for (const auto& entry : mapWallet) { const CWalletTx* pcoin = &entry.second; - if (!CheckFinalTx(*pcoin) || (!pcoin->IsTrusted() && pcoin->GetDepthInMainChain() == 0)) - nTotal += pcoin->GetAvailableCredit(); + if (!CheckFinalTx(*pcoin) || (!pcoin->IsTrusted(std::nullopt) && pcoin->GetDepthInMainChain(std::nullopt) == 0)) + nTotal += pcoin->GetAvailableCredit(std::nullopt); } } return nTotal; } -CAmount CWallet::GetImmatureBalance() const +CAmount CWallet::GetImmatureBalance(const std::optional& asOfHeight) const { CAmount nTotal = 0; { @@ -5165,36 +5170,7 @@ CAmount CWallet::GetImmatureBalance() const for (const auto& entry : mapWallet) { const CWalletTx* pcoin = &entry.second; - nTotal += pcoin->GetImmatureCredit(); - } - } - return nTotal; -} - -CAmount CWallet::GetUnconfirmedWatchOnlyBalance() const -{ - CAmount nTotal = 0; - { - LOCK2(cs_main, cs_wallet); - for (const auto& entry : mapWallet) - { - const CWalletTx* pcoin = &entry.second; - if (!CheckFinalTx(*pcoin) || (!pcoin->IsTrusted() && pcoin->GetDepthInMainChain() == 0)) - nTotal += pcoin->GetAvailableCredit(true, ISMINE_WATCH_ONLY); - } - } - return nTotal; -} - -CAmount CWallet::GetImmatureWatchOnlyBalance() const -{ - CAmount nTotal = 0; - { - LOCK2(cs_main, cs_wallet); - for (const auto& entry : mapWallet) - { - const CWalletTx* pcoin = &entry.second; - nTotal += pcoin->GetImmatureWatchOnlyCredit(); + nTotal += pcoin->GetImmatureCredit(asOfHeight); } } return nTotal; @@ -5213,8 +5189,8 @@ CAmount CWallet::GetLegacyBalance(const isminefilter& filter, int minDepth) cons CAmount balance = 0; for (const auto& entry : mapWallet) { const CWalletTx& wtx = entry.second; - const int depth = wtx.GetDepthInMainChain(); - if (depth < 0 || !CheckFinalTx(wtx) || wtx.GetBlocksToMaturity() > 0) { + const int depth = wtx.GetDepthInMainChain(std::nullopt); + if (depth < 0 || !CheckFinalTx(wtx) || wtx.GetBlocksToMaturity(std::nullopt) > 0) { continue; } @@ -5240,45 +5216,43 @@ CAmount CWallet::GetLegacyBalance(const isminefilter& filter, int minDepth) cons } void CWallet::AvailableCoins(vector& vCoins, + const std::optional& asOfHeight, bool fOnlyConfirmed, const CCoinControl *coinControl, bool fIncludeZeroValue, bool fIncludeCoinBase, bool fOnlySpendable, int nMinDepth, - std::set* onlyFilterByDests) const + const std::set& onlyFilterByDests) const { assert(nMinDepth >= 0); + assert(!asOfHeight.has_value() || nMinDepth > 0); AssertLockHeld(cs_main); AssertLockHeld(cs_wallet); vCoins.clear(); { - for (map::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) - { - const uint256& wtxid = it->first; - const CWalletTx* pcoin = &(*it).second; - - if (!CheckFinalTx(*pcoin)) + for (const auto& [wtxid, pcoin] : mapWallet) { + if (!CheckFinalTx(pcoin)) continue; - if (fOnlyConfirmed && !pcoin->IsTrusted()) + if (fOnlyConfirmed && !pcoin.IsTrusted(asOfHeight)) continue; - if (pcoin->IsCoinBase() && !fIncludeCoinBase) + if (pcoin.IsCoinBase() && !fIncludeCoinBase) continue; - bool isCoinbase = pcoin->IsCoinBase(); - if (isCoinbase && pcoin->GetBlocksToMaturity() > 0) + bool isCoinbase = pcoin.IsCoinBase(); + if (isCoinbase && pcoin.GetBlocksToMaturity(asOfHeight) > 0) continue; - int nDepth = pcoin->GetDepthInMainChain(); + int nDepth = pcoin.GetDepthInMainChain(asOfHeight); if (nDepth < nMinDepth) continue; - for (unsigned int i = 0; i < pcoin->vout.size(); i++) { - const auto& output = pcoin->vout[i]; + for (unsigned int i = 0; i < pcoin.vout.size(); i++) { + const auto& output = pcoin.vout[i]; isminetype mine = IsMine(output); bool isSpendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || @@ -5288,17 +5262,17 @@ void CWallet::AvailableCoins(vector& vCoins, continue; // Filter by specific destinations if needed - if (onlyFilterByDests && !onlyFilterByDests->empty()) { + if (!onlyFilterByDests.empty()) { CTxDestination address; - if (!ExtractDestination(output.scriptPubKey, address) || onlyFilterByDests->count(address) == 0) { + if (!ExtractDestination(output.scriptPubKey, address) || onlyFilterByDests.count(address) == 0) { continue; } } - if (!(IsSpent(wtxid, i)) && mine != ISMINE_NO && - !IsLockedCoin((*it).first, i) && (pcoin->vout[i].nValue > 0 || fIncludeZeroValue) && - (!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected((*it).first, i))) - vCoins.push_back(COutput(pcoin, i, nDepth, isSpendable, isCoinbase)); + if (!(IsSpent(wtxid, i, asOfHeight)) && mine != ISMINE_NO && + !IsLockedCoin(wtxid, i) && (pcoin.vout[i].nValue > 0 || fIncludeZeroValue) && + (!coinControl || !coinControl->HasSelected() || coinControl->fAllowOtherInputs || coinControl->IsSelected(wtxid, i))) + vCoins.push_back(COutput(&pcoin, i, nDepth, isSpendable, isCoinbase)); } } } @@ -5455,8 +5429,8 @@ bool CWallet::SelectCoins(const CAmount& nTargetValue, set vCoinsNoCoinbase, vCoinsWithCoinbase; - AvailableCoins(vCoinsNoCoinbase, true, coinControl, false, false); - AvailableCoins(vCoinsWithCoinbase, true, coinControl, false, true); + AvailableCoins(vCoinsNoCoinbase, std::nullopt, true, coinControl, false, false); + AvailableCoins(vCoinsWithCoinbase, std::nullopt, true, coinControl, false, true); fOnlyCoinbaseCoinsRet = vCoinsNoCoinbase.size() == 0 && vCoinsWithCoinbase.size() > 0; // If coinbase utxos can only be sent to zaddrs, exclude any coinbase utxos from coin selection. @@ -5725,7 +5699,7 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt //reflecting an assumption the user would accept a bit more delay for //a chance at a free transaction. //But mempool inputs might still be in the mempool, so their age stays 0 - int age = pcoin.first->GetDepthInMainChain(); + int age = pcoin.first->GetDepthInMainChain(std::nullopt); if (age != 0) age += 1; dPriority += (double)nCredit * age; @@ -6220,7 +6194,7 @@ int64_t CWallet::GetOldestKeyPoolTime() return keypool.nTime; } -std::map CWallet::GetAddressBalances() +std::map CWallet::GetAddressBalances(const std::optional& asOfHeight) { map balances; @@ -6230,13 +6204,13 @@ std::map CWallet::GetAddressBalances() { CWalletTx *pcoin = &walletEntry.second; - if (!CheckFinalTx(*pcoin) || !pcoin->IsTrusted()) + if (!CheckFinalTx(*pcoin) || !pcoin->IsTrusted(asOfHeight)) continue; - if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity() > 0) + if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity(asOfHeight) > 0) continue; - int nDepth = pcoin->GetDepthInMainChain(); + int nDepth = pcoin->GetDepthInMainChain(asOfHeight); if (nDepth < (pcoin->IsFromMe(ISMINE_ALL) ? 0 : 1)) continue; @@ -6248,7 +6222,7 @@ std::map CWallet::GetAddressBalances() if(!ExtractDestination(pcoin->vout[i].scriptPubKey, addr)) continue; - CAmount n = IsSpent(walletEntry.first, i) ? 0 : pcoin->vout[i].nValue; + CAmount n = IsSpent(walletEntry.first, i, asOfHeight) ? 0 : pcoin->vout[i].nValue; if (!balances.count(addr)) balances[addr] = 0; @@ -6984,39 +6958,43 @@ void CMerkleTx::SetMerkleBranch(const CBlock& block) } } -int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const +int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet, const std::optional& asOfHeight) const { if (hashBlock.IsNull() || nIndex == -1) return 0; AssertLockHeld(cs_main); + int effectiveChainHeight = min(chainActive.Height(), asOfHeight.value_or(chainActive.Height())); // Find the block it claims to be in BlockMap::iterator mi = mapBlockIndex.find(hashBlock); if (mi == mapBlockIndex.end()) return 0; CBlockIndex* pindex = (*mi).second; - if (!pindex || !chainActive.Contains(pindex)) + if (!pindex || + !chainActive.Contains(pindex) || + pindex->nHeight > effectiveChainHeight) { return 0; + } pindexRet = pindex; - return chainActive.Height() - pindex->nHeight + 1; + return effectiveChainHeight - pindex->nHeight + 1; } -int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet) const +int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet, const std::optional& asOfHeight) const { AssertLockHeld(cs_main); - int nResult = GetDepthInMainChainINTERNAL(pindexRet); - if (nResult == 0 && !mempool.exists(GetHash())) + int nResult = GetDepthInMainChainINTERNAL(pindexRet, asOfHeight); + if (nResult == 0 && (asOfHeight.has_value() || !mempool.exists(GetHash()))) return -1; // Not in chain, not in mempool return nResult; } -int CMerkleTx::GetBlocksToMaturity() const +int CMerkleTx::GetBlocksToMaturity(const std::optional& asOfHeight) const { if (!IsCoinBase()) return 0; - return max(0, (COINBASE_MATURITY+1) - GetDepthInMainChain()); + return max(0, (COINBASE_MATURITY+1) - GetDepthInMainChain(asOfHeight)); } @@ -7098,6 +7076,7 @@ void CWallet::GetFilteredNotes( std::vector& saplingEntriesRet, std::vector& orchardNotesRet, const std::optional& noteFilter, + const std::optional& asOfHeight, int minDepth, int maxDepth, bool ignoreSpent, @@ -7116,8 +7095,8 @@ void CWallet::GetFilteredNotes( // Filter the transactions before checking for notes if (!CheckFinalTx(wtx) || - wtx.GetDepthInMainChain() < minDepth || - wtx.GetDepthInMainChain() > maxDepth) { + wtx.GetDepthInMainChain(asOfHeight) < minDepth || + wtx.GetDepthInMainChain(asOfHeight) > maxDepth) { continue; } @@ -7137,7 +7116,7 @@ void CWallet::GetFilteredNotes( } // skip note which has been spent - if (ignoreSpent && nd.nullifier && IsSproutSpent(*nd.nullifier)) { + if (ignoreSpent && nd.nullifier && IsSproutSpent(*nd.nullifier, asOfHeight)) { continue; } @@ -7175,7 +7154,7 @@ void CWallet::GetFilteredNotes( (unsigned char) j); sproutEntriesRet.push_back(SproutNoteEntry { - jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain() }); + jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain(asOfHeight) }); } catch (const note_decryption_failed &err) { // Couldn't decrypt with this spending key @@ -7206,7 +7185,7 @@ void CWallet::GetFilteredNotes( continue; } - if (ignoreSpent && nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value())) { + if (ignoreSpent && nd.nullifier.has_value() && IsSaplingSpent(nd.nullifier.value(), asOfHeight)) { continue; } @@ -7222,7 +7201,7 @@ void CWallet::GetFilteredNotes( auto note = notePt.note(nd.ivk).value(); saplingEntriesRet.push_back(SaplingNoteEntry { - op, pa, note, notePt.memo(), wtx.GetDepthInMainChain() }); + op, pa, note, notePt.memo(), wtx.GetDepthInMainChain(asOfHeight) }); } } @@ -7248,13 +7227,13 @@ void CWallet::GetFilteredNotes( } for (auto& noteMeta : orchardNotes) { - if (ignoreSpent && IsOrchardSpent(noteMeta.GetOutPoint())) { + if (ignoreSpent && IsOrchardSpent(noteMeta.GetOutPoint(), asOfHeight)) { continue; } auto wtx = GetWalletTx(noteMeta.GetOutPoint().hash); if (wtx) { - auto confirmations = wtx->GetDepthInMainChain(); + auto confirmations = wtx->GetDepthInMainChain(asOfHeight); if (confirmations >= minDepth && confirmations <= maxDepth) { noteMeta.SetConfirmations(confirmations); orchardNotesRet.push_back(noteMeta); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 795ebc57c..39ecc59c0 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -399,7 +399,11 @@ struct SaplingNoteEntry class CMerkleTx : public CTransaction { private: - int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const; + /** + * **NB**: Unlike `GetDepthInMainChain`, this returns 0 for any case where + * it’s not in the chain (including if it’s not in the mempool). + */ + int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet, const std::optional& asOfHeight) const; public: uint256 hashBlock; @@ -438,13 +442,16 @@ public: /** * Return depth of transaction in blockchain: * -1 : not in blockchain, and not in memory pool (conflicted transaction) - * 0 : in memory pool, waiting to be included in a block + * 0 : in memory pool, waiting to be included in a block (never returned if `asOfHeight` is set) * >=1 : this many blocks deep in the main chain */ - int GetDepthInMainChain(const CBlockIndex* &pindexRet) const; - int GetDepthInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChain(pindexRet); } - bool IsInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChainINTERNAL(pindexRet) > 0; } - int GetBlocksToMaturity() const; + int GetDepthInMainChain(const CBlockIndex* &pindexRet, const std::optional& asOfHeight) const; + int GetDepthInMainChain(const std::optional& asOfHeight) const { + const CBlockIndex *pindexRet; + return GetDepthInMainChain(pindexRet, asOfHeight); + } + bool IsInMainChain(const std::optional& asOfHeight) const { return GetDepthInMainChain(asOfHeight) > 0; } + int GetBlocksToMaturity(const std::optional& asOfHeight) const; /** Pass this transaction to the mempool. Fails if absolute fee exceeds maxTxFee. */ bool AcceptToMemoryPool(CValidationState& state, bool fLimitFree=true, bool fRejectAbsurdFee=true); }; @@ -670,10 +677,10 @@ public: //! filter decides which addresses will count towards the debit CAmount GetDebit(const isminefilter& filter) const; - CAmount GetCredit(const isminefilter& filter) const; - CAmount GetImmatureCredit(bool fUseCache=true) const; - CAmount GetAvailableCredit(bool fUseCache=true, const isminefilter& filter=ISMINE_SPENDABLE) const; - CAmount GetImmatureWatchOnlyCredit(const bool fUseCache=true) const; + CAmount GetCredit(const std::optional& asOfHeight, const isminefilter& filter) const; + CAmount GetImmatureCredit(const std::optional& asOfHeight, bool fUseCache=true) const; + CAmount GetAvailableCredit(const std::optional& asOfHeight, bool fUseCache=true, const isminefilter& filter=ISMINE_SPENDABLE) const; + CAmount GetImmatureWatchOnlyCredit(const std::optional& asOfHeight, const bool fUseCache=true) const; CAmount GetChange() const; void GetAmounts(std::list& listReceived, @@ -681,7 +688,7 @@ public: bool IsFromMe(const isminefilter& filter) const; - bool IsTrusted() const; + bool IsTrusted(const std::optional& asOfHeight) const; int64_t GetTxTime() const; int GetRequestCount() const; @@ -1428,15 +1435,18 @@ public: /** * populate vCoins with vector of available COutputs. + * + * **NB**: If `asOfHeight` is specified, then `nMinDepth` must be `> 0`. */ void AvailableCoins(std::vector& vCoins, + const std::optional& asOfHeight, bool fOnlyConfirmed=true, const CCoinControl *coinControl = NULL, bool fIncludeZeroValue=false, bool fIncludeCoinBase=true, bool fOnlySpendable=false, int nMinDepth = 0, - std::set* onlyFilterByDests = nullptr) const; + const std::set& onlyFilterByDests = std::set()) const; /** * Shuffle and select coins until nTargetValue is reached while avoiding @@ -1516,16 +1526,17 @@ public: SpendableInputs FindSpendableInputs( ZTXOSelector paymentSource, bool allowTransparentCoinbase, - uint32_t minDepth) const; + uint32_t minDepth, + const std::optional& asOfHeight) const; bool SelectorMatchesAddress(const ZTXOSelector& source, const CTxDestination& a0) const; bool SelectorMatchesAddress(const ZTXOSelector& source, const libzcash::SproutPaymentAddress& a0) const; bool SelectorMatchesAddress(const ZTXOSelector& source, const libzcash::SaplingPaymentAddress& a0) const; - bool IsSpent(const uint256& hash, unsigned int n) const; - bool IsSproutSpent(const uint256& nullifier) const; - bool IsSaplingSpent(const uint256& nullifier) const; - bool IsOrchardSpent(const OrchardOutPoint& outpoint) const; + bool IsSpent(const uint256& hash, unsigned int n, const std::optional& asOfHeight) const; + bool IsSproutSpent(const uint256& nullifier, const std::optional& asOfHeight) const; + bool IsSaplingSpent(const uint256& nullifier, const std::optional& asOfHeight) const; + bool IsOrchardSpent(const OrchardOutPoint& outpoint, const std::optional& asOfHeight) const; bool IsLockedCoin(uint256 hash, unsigned int n) const; void LockCoin(COutPoint& output); @@ -1793,11 +1804,14 @@ public: void ReacceptWalletTransactions(); void ResendWalletTransactions(int64_t nBestBlockTime); std::vector ResendWalletTransactionsBefore(int64_t nTime); - CAmount GetBalance(const isminefilter& filter=ISMINE_SPENDABLE, const int min_depth=0) const; - CAmount GetUnconfirmedBalance() const; - CAmount GetImmatureBalance() const; - CAmount GetUnconfirmedWatchOnlyBalance() const; - CAmount GetImmatureWatchOnlyBalance() const; + CAmount GetBalance(const std::optional& asOfHeight, + const isminefilter& filter=ISMINE_SPENDABLE, + const int min_depth=0) const; + /** + * Returns the balance taking into account _only_ transactions in the mempool. + */ + CAmount GetUnconfirmedTransparentBalance() const; + CAmount GetImmatureBalance(const std::optional& asOfHeight) const; CAmount GetLegacyBalance(const isminefilter& filter, int minDepth) const; /** @@ -1872,7 +1886,7 @@ public: void GetAllReserveKeys(std::set& setAddress) const; std::set< std::set > GetAddressGroupings(); - std::map GetAddressBalances(); + std::map GetAddressBalances(const std::optional& asOfHeight); std::optional GetSproutNoteNullifier( const JSDescription& jsdesc, @@ -2079,6 +2093,7 @@ public: std::vector& saplingEntriesRet, std::vector& orchardNotesRet, const std::optional& noteFilter, + const std::optional& asOfHeight, int minDepth, int maxDepth=INT_MAX, bool ignoreSpent=true, diff --git a/test/lint/commit-script-check.sh b/test/lint/commit-script-check.sh index 433e78393..8b55f1578 100755 --- a/test/lint/commit-script-check.sh +++ b/test/lint/commit-script-check.sh @@ -13,14 +13,14 @@ # one. Any remaining diff signals an error. export LC_ALL=C -if test "x$1" = "x"; then +if test -z "$1"; then echo "Usage: $0 ..." exit 1 fi # Check that the working tree is clean (as this script performs hard resets). TREE_CHANGES=$(git diff HEAD | wc -l) -if test "x$TREE_CHANGES" != "x0"; then +if test "$TREE_CHANGES" != "0"; then echo "The working tree is not clean." exit 1 fi @@ -32,7 +32,7 @@ for commit in $(git rev-list --reverse $1); do if git rev-list -n 1 --pretty="%s" $commit | grep -q "^scripted-diff:"; then git checkout --quiet $commit^ || exit SCRIPT="$(git rev-list --format=%b -n1 $commit | sed '/^-BEGIN VERIFY SCRIPT-$/,/^-END VERIFY SCRIPT-$/{//!b};d')" - if test "x$SCRIPT" = "x"; then + if test -z "$SCRIPT"; then echo "Error: missing script for: $commit" echo "Failed" RET=1 diff --git a/test/lint/lint-shell.sh b/test/lint/lint-shell.sh index ba0c906de..8ae62818f 100755 --- a/test/lint/lint-shell.sh +++ b/test/lint/lint-shell.sh @@ -40,7 +40,7 @@ if ! command -v shellcheck > /dev/null; then fi EXCLUDE="--exclude=$(IFS=','; echo "${disabled[*]}")" -if ! shellcheck "$EXCLUDE" $(git ls-files -- '*.sh' | grep -vE 'src/(leveldb|secp256k1|univalue)/'); then +if ! shellcheck "$EXCLUDE" $(git ls-files -- '*.sh' | grep -vE '(qa/zcash/checksec\.sh|src/(|leveldb|secp256k1|univalue)/)'); then EXIT_CODE=1 fi diff --git a/zcutil/build.sh b/zcutil/build.sh index 3a7c32192..5032a9f0e 100755 --- a/zcutil/build.sh +++ b/zcutil/build.sh @@ -40,7 +40,7 @@ if [ -z "${CONFIGURE_FLAGS-}" ]; then CONFIGURE_FLAGS="" fi -if [ "x$*" = 'x--help' ] +if [ "$*" = '--help' ] then cat <