diff --git a/configure.ac b/configure.ac index 8e0af75224..6a67994dcb 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ AC_PREREQ([2.69]) define(_CLIENT_VERSION_MAJOR, 23) define(_CLIENT_VERSION_MINOR, 3) define(_CLIENT_VERSION_BUILD, 4) -define(_CLIENT_VERSION_RC, 1) +define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) define(_COPYRIGHT_YEAR, 2026) define(_COPYRIGHT_HOLDERS,[The %s developers]) diff --git a/doc/man/elements-cli.1 b/doc/man/elements-cli.1 index 111ee9e722..908f570fd2 100644 --- a/doc/man/elements-cli.1 +++ b/doc/man/elements-cli.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH ELEMENTS-CLI "1" "June 2026" "elements-cli v23.3.4" "User Commands" +.TH ELEMENTS-CLI "1" "September 2026" "elements-cli v23.3.4" "User Commands" .SH NAME elements-cli \- manual page for elements-cli v23.3.4 .SH SYNOPSIS diff --git a/doc/man/elements-qt.1 b/doc/man/elements-qt.1 index 97a47e1b5c..a25faf1866 100644 --- a/doc/man/elements-qt.1 +++ b/doc/man/elements-qt.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH ELEMENTS-QT "1" "June 2026" "elements-qt v23.3.4" "User Commands" +.TH ELEMENTS-QT "1" "September 2026" "elements-qt v23.3.4" "User Commands" .SH NAME elements-qt \- manual page for elements-qt v23.3.4 .SH SYNOPSIS @@ -115,7 +115,7 @@ Do not keep transactions in the mempool longer than hours (default: .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-16\fR to 15, 0 = auto, <0 = +Set the number of script verification threads (\fB\-24\fR to 15, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-persistmempool\fR @@ -318,10 +318,6 @@ created within past week. 0 = no limit (default: 0M). Optional suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 base while uppercase is 1024 base .HP -\fB\-natpmp\fR -.IP -Use NAT\-PMP to map the listening port (default: 0) -.HP \fB\-networkactive\fR .IP Enable all P2P network activity (default: 1). Can be changed by the @@ -390,10 +386,6 @@ Tor control port to use if onion listening enabled (default: .IP Tor control port password (default: empty) .HP -\fB\-upnp\fR -.IP -Use UPnP to map the listening port (default: 0) -.HP \fB\-whitebind=\fR<[permissions@]addr> .IP Bind to the given address and add permission flags to the peers diff --git a/doc/man/elements-tx.1 b/doc/man/elements-tx.1 index 04f0cbf7d5..f3123e3a6f 100644 --- a/doc/man/elements-tx.1 +++ b/doc/man/elements-tx.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH ELEMENTS-TX "1" "June 2026" "elements-tx v23.3.4" "User Commands" +.TH ELEMENTS-TX "1" "September 2026" "elements-tx v23.3.4" "User Commands" .SH NAME elements-tx \- manual page for elements-tx v23.3.4 .SH SYNOPSIS diff --git a/doc/man/elements-util.1 b/doc/man/elements-util.1 index e73f293f6d..861537751a 100644 --- a/doc/man/elements-util.1 +++ b/doc/man/elements-util.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH ELEMENTS-UTIL "1" "June 2026" "elements-util v23.3.4" "User Commands" +.TH ELEMENTS-UTIL "1" "September 2026" "elements-util v23.3.4" "User Commands" .SH NAME elements-util \- manual page for elements-util v23.3.4 .SH SYNOPSIS diff --git a/doc/man/elements-wallet.1 b/doc/man/elements-wallet.1 index 33a0328632..61ab61980c 100644 --- a/doc/man/elements-wallet.1 +++ b/doc/man/elements-wallet.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH ELEMENTS-WALLET "1" "June 2026" "elements-wallet v23.3.4" "User Commands" +.TH ELEMENTS-WALLET "1" "September 2026" "elements-wallet v23.3.4" "User Commands" .SH NAME elements-wallet \- manual page for elements-wallet v23.3.4 .SH DESCRIPTION diff --git a/doc/man/elementsd.1 b/doc/man/elementsd.1 index 075185be0b..654cdbde1c 100644 --- a/doc/man/elementsd.1 +++ b/doc/man/elementsd.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH ELEMENTSD "1" "June 2026" "elementsd v23.3.4" "User Commands" +.TH ELEMENTSD "1" "September 2026" "elementsd v23.3.4" "User Commands" .SH NAME elementsd \- manual page for elementsd v23.3.4 .SH SYNOPSIS @@ -115,7 +115,7 @@ Do not keep transactions in the mempool longer than hours (default: .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-16\fR to 15, 0 = auto, <0 = +Set the number of script verification threads (\fB\-24\fR to 15, 0 = auto, <0 = leave that many cores free, default: 0) .HP \fB\-persistmempool\fR @@ -318,10 +318,6 @@ created within past week. 0 = no limit (default: 0M). Optional suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 base while uppercase is 1024 base .HP -\fB\-natpmp\fR -.IP -Use NAT\-PMP to map the listening port (default: 0) -.HP \fB\-networkactive\fR .IP Enable all P2P network activity (default: 1). Can be changed by the @@ -390,10 +386,6 @@ Tor control port to use if onion listening enabled (default: .IP Tor control port password (default: empty) .HP -\fB\-upnp\fR -.IP -Use UPnP to map the listening port (default: 0) -.HP \fB\-whitebind=\fR<[permissions@]addr> .IP Bind to the given address and add permission flags to the peers diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 24631f8450..7b0ae72b0a 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -132,6 +132,7 @@ BITCOIN_TESTS =\ test/serialize_tests.cpp \ test/settings_tests.cpp \ test/sighash_tests.cpp \ + test/sigcache_tests.cpp \ test/sigopcount_tests.cpp \ test/skiplist_tests.cpp \ test/sock_tests.cpp \ diff --git a/src/init.cpp b/src/init.cpp index d83d9bd463..217a384061 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -556,6 +556,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-capturemessages", "Capture all P2P messages to disk", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-mocktime=", "Replace actual time with " + UNIX_EPOCH_TIME + " (default: 0)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-maxsigcachesize=", strprintf("Limit sum of signature cache and script execution cache sizes to MiB (default: %u)", DEFAULT_MAX_SIG_CACHE_SIZE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-rangeproofcache", strprintf("Enable the range proof validation cache (default: %u). Use -norangeproofcache to disable.", 1), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-maxtipage=", strprintf("Maximum tip age in seconds to consider node in initial block download (default: %u)", DEFAULT_MAX_TIP_AGE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-printpriority", strprintf("Log transaction fee rate in " + CURRENCY_UNIT + "/kvB when mining blocks (default: %u)", DEFAULT_PRINTPRIORITY), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-uacomment=", "Append comment to the user agent string", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); diff --git a/src/script/sigcache.cpp b/src/script/sigcache.cpp index 9f7bb9592b..991ebd043e 100644 --- a/src/script/sigcache.cpp +++ b/src/script/sigcache.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -26,23 +27,23 @@ namespace { class CSignatureCache { private: - //! Entries are SHA256(nonce || 'E' or 'S' || 31 zero bytes || signature hash || public key || signature): + //! Salted SHA256 midstates, domain-separated by signature or proof type. CSHA256 m_salted_hasher_ecdsa; CSHA256 m_salted_hasher_schnorr; - CSHA256 m_salted_hasher_range_proof; - CSHA256 m_salted_hasher_surjection_proof; + CHashWriter m_salted_hasher_range_proof; + CHashWriter m_salted_hasher_surjection_proof; typedef CuckooCache::cache map_type; map_type setValid; std::shared_mutex cs_sigcache; public: - CSignatureCache() + CSignatureCache(): + m_salted_hasher_range_proof(SER_GETHASH,0), + m_salted_hasher_surjection_proof(SER_GETHASH,0) { uint256 nonce = GetRandHash(); - // We want the nonce to be 64 bytes long to force the hasher to process - // this chunk, which makes later hash computations more efficient. We - // just write our 32-byte entropy, and then pad with 'E' for ECDSA and - // 'S' for Schnorr (followed by 0 bytes). + // Use 64-byte, type-specific salted midstates so later hash computations + // can start after the first SHA256 chunk. static constexpr unsigned char PADDING_ECDSA[32] = {'E'}; static constexpr unsigned char PADDING_SCHNORR[32] = {'S'}; static constexpr unsigned char PADDING_RANGE_PROOF[32] = {'r'}; @@ -51,10 +52,8 @@ class CSignatureCache m_salted_hasher_ecdsa.Write(PADDING_ECDSA, 32); m_salted_hasher_schnorr.Write(nonce.begin(), 32); m_salted_hasher_schnorr.Write(PADDING_SCHNORR, 32); - m_salted_hasher_range_proof.Write(nonce.begin(), 32); - m_salted_hasher_range_proof.Write(PADDING_RANGE_PROOF, 32); - m_salted_hasher_surjection_proof.Write(nonce.begin(), 32); - m_salted_hasher_surjection_proof.Write(PADDING_SURJECTION_PROOF, 32); + m_salted_hasher_range_proof << nonce << PADDING_RANGE_PROOF; + m_salted_hasher_surjection_proof << nonce << PADDING_SURJECTION_PROOF; } void @@ -72,13 +71,39 @@ class CSignatureCache } // ELEMENTS: - void ComputeEntryRangeProof(uint256& entry, const std::vector& proof, const std::vector& commitment, const std::vector& asset_commitment, const CScript& scriptPubKey) { - CSHA256 hasher = m_salted_hasher_range_proof; - hasher.Write(proof.data(), proof.size()).Write(commitment.data(), commitment.size()).Write(asset_commitment.data(), asset_commitment.size()).Write(scriptPubKey.data(), scriptPubKey.size()).Finalize(entry.begin()); + void ComputeEntryRangeProof(uint256& entry, + const std::vector& proof, + const std::vector& commitment, + const std::vector& asset_commitment, + const CScript& script_pub_key) const + { + CHashWriter hasher = m_salted_hasher_range_proof; + // We commit to both commitments and the scriptPubKey because these are + // committed to by the rangeproof itself; a change in any of them would + // invalidate the proof. Since these are exactly the arguments to + // CachingRangeProofChecker::VerifyRangeProof (below), there is no + // additional data that could affect the rangeproof's validity. + // Serialization length-prefixes every field, including the variable-length + // proof and script, so distinct argument tuples cannot share an encoding. + hasher << proof << commitment << asset_commitment << script_pub_key; + entry = hasher.GetSHA256(); } - void ComputeEntrySurjectionProof(uint256& entry, const uint256 &hash, const std::vector& proof, const std::vector& commitment) { - CSHA256 hasher = m_salted_hasher_surjection_proof; - hasher.Write(hash.begin(), 32).Write(proof.data(), proof.size()).Write(commitment.data(), commitment.size()).Finalize(entry.begin()); + void ComputeEntrySurjectionProof(uint256& entry, const uint256 &hash, const std::vector& proof, const std::vector& commitment, const std::vector& vTags) const { + CHashWriter hasher = m_salted_hasher_surjection_proof; + // We hash all arguments passed to CachingSurjectionProofChecker::VerifySurjectionProof, + // to ensure that any change in the way that the verification function is called will + // trigger a cache miss and explicit verification. However, we note that the `wtxid` + // (hash) commits to all the other data such that we could technically hash only it. + // We retain the other data as a defense against future refactorings. + // + // Serialize vTags as a flat byte vector (each secp256k1_generator is 64 bytes). + std::vector vTagsBytes; + vTagsBytes.reserve(vTags.size() * 64); + for (const auto& tag : vTags) { + vTagsBytes.insert(vTagsBytes.end(), std::begin(tag.data), std::end(tag.data)); + } + hasher << hash << proof << commitment << vTagsBytes; + entry = hasher.GetSHA256(); } bool @@ -154,6 +179,10 @@ bool CachingTransactionSignatureChecker::VerifySchnorrSignature(Span& vchRangeProof, const std::vector& vchValueCommitment, const std::vector& vchAssetCommitment, const CScript& scriptPubKey, const secp256k1_context* secp256k1_ctx_verify_amounts) const { + // ELEMENTS: NOTE FOR FUTURE EDITORS: every argument to this function that + // carries data (i.e. everything except the secp256k1 context, which is + // stateless) MUST be included in ComputeEntryRangeProof. Omitting any + // argument risks returning a cached positive result for a proof that was + // verified with different inputs. uint256 entry; - rangeProofCache.ComputeEntryRangeProof(entry, vchRangeProof, vchValueCommitment, vchAssetCommitment, scriptPubKey); - - if (rangeProofCache.Get(entry, !store)) { - return true; + const bool useCache = gArgs.GetBoolArg("-rangeproofcache", true); + if (useCache) { + rangeProofCache.ComputeEntryRangeProof(entry, vchRangeProof, vchValueCommitment, vchAssetCommitment, scriptPubKey); + if (rangeProofCache.Get(entry, !store)) { + return true; + } } if (vchRangeProof.size() == 0) { @@ -208,7 +244,7 @@ bool CachingRangeProofChecker::VerifyRangeProof(const std::vector return false; } - if (store) { + if (useCache && store) { rangeProofCache.Set(entry); } @@ -227,7 +263,7 @@ bool CachingSurjectionProofChecker::VerifySurjectionProof(secp256k1_surjectionpr // wtxid commits to all data including surj targets // we need to specify the proof and output asset point to be unique uint256 entry; - surjectionProofCache.ComputeEntrySurjectionProof(entry, wtxid, vchproof, std::vector(std::begin(gen.data), std::end(gen.data))); + surjectionProofCache.ComputeEntrySurjectionProof(entry, wtxid, vchproof, std::vector(std::begin(gen.data), std::end(gen.data)), vTags); if (surjectionProofCache.Get(entry, !store)) { return true; @@ -244,5 +280,24 @@ bool CachingSurjectionProofChecker::VerifySurjectionProof(secp256k1_surjectionpr return true; } +// Test-only hooks (see sigcache.h). Forward to the anonymous-namespace caches. +void TestComputeEntryRangeProof(uint256& entry, + const std::vector& proof, + const std::vector& commitment, + const std::vector& asset_commitment, + const CScript& script_pub_key) +{ + rangeProofCache.ComputeEntryRangeProof(entry, proof, commitment, asset_commitment, script_pub_key); +} + +void TestComputeEntrySurjectionProof(uint256& entry, + const uint256& hash, + const std::vector& proof, + const std::vector& commitment, + const std::vector& vTags) +{ + surjectionProofCache.ComputeEntrySurjectionProof(entry, hash, proof, commitment, vTags); +} + // END ELEMENTS // diff --git a/src/script/sigcache.h b/src/script/sigcache.h index 0c6477edec..e91bded9c9 100644 --- a/src/script/sigcache.h +++ b/src/script/sigcache.h @@ -70,6 +70,20 @@ class CachingSurjectionProofChecker void InitRangeproofCache(); void InitSurjectionproofCache(); +// Test-only hooks: expose the (anonymous-namespace) cache-entry computation so +// unit tests can verify collision-resistance and domain separation. These are +// NOT part of the consensus/validation API and are only used by unit tests. +void TestComputeEntryRangeProof(uint256& entry, + const std::vector& proof, + const std::vector& commitment, + const std::vector& asset_commitment, + const CScript& script_pub_key); +void TestComputeEntrySurjectionProof(uint256& entry, + const uint256& hash, + const std::vector& proof, + const std::vector& commitment, + const std::vector& vTags); + // END ELEMENTS // diff --git a/src/test/sigcache_tests.cpp b/src/test/sigcache_tests.cpp new file mode 100644 index 0000000000..6cd13c5fbb --- /dev/null +++ b/src/test/sigcache_tests.cpp @@ -0,0 +1,164 @@ +// Copyright (c) 2026 The Elements developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// +// Tests for the Elements proof-cache entry computation (script/sigcache.cpp). +// +// These tests guard the collision-resistance and domain-separation properties +// of the cache keys used for the range-proof and surjection-proof caches. +// A cache entry is a *positive* verification result, so a key collision means +// accepting a proof without ever verifying it. The keys are computed with +// CHashWriter serialization, which length-prefixes every field, so two +// distinct argument tuples must never produce the same cache entry, and the +// two proof types must live in disjoint key spaces (domain separation). + +#include