From ac95c5d5139c839b9cf561a025a29ef663ddd25f Mon Sep 17 00:00:00 2001 From: andycreed0x <11888868+andycreed0x@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:16:04 -0300 Subject: [PATCH] Delimit rangeproof cache key fields to prevent boundary collisions ComputeEntryRangeProof hashed (proof, value commitment, asset commitment, scriptPubKey) into the cache key by raw concatenation, with no field delimiters. The two commitments are fixed 33-byte fields, but proof and scriptPubKey are variable length and sit at opposite ends of the stream, so the proof/script boundary can be shifted while leaving the concatenated bytes identical. Two distinct tuples then map to the same key. Because a positive cache hit returns true without verifying, a node that had cached one valid rangeproof would accept a different, unverified proof whose (proof, script) split differs but whose byte stream matches: the attacker primes the cache with a genuine proof over an OP_RETURN script carrying padding, then resubmits the same bytes re-split so the padding counts as proof and the script shrinks to a single byte. Prepend the four field lengths to the hashed key so the encoding is injective; no other partition of the same stream yields the same key. The key is process-local (salted per start, never serialized or compared across nodes), so the native byte order of the length array is fine. Add a regression test (blind_tests/rangeproof_cache_key_field_boundary) that primes the cache with a genuine proof over a 69-byte OP_RETURN script, then submits an attack tuple that shifts the boundary 68 bytes; the attack proof is invalid for its one-byte script and must be rejected. --- src/script/sigcache.cpp | 4 ++- src/test/blind_tests.cpp | 76 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/script/sigcache.cpp b/src/script/sigcache.cpp index 9f7bb9592b..a5c9198239 100644 --- a/src/script/sigcache.cpp +++ b/src/script/sigcache.cpp @@ -74,7 +74,9 @@ 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()); + // Commit to field lengths first: without them, distinct (proof, commitment, asset_commitment, scriptPubKey) tuples that concatenate to the same byte stream collide to one cache key. The key is process-local (salted per start, never serialized or compared across nodes), so the array's native byte order is fine. + const uint64_t lengths[4] = {proof.size(), commitment.size(), asset_commitment.size(), scriptPubKey.size()}; + hasher.Write(reinterpret_cast(lengths), sizeof(lengths)).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 ComputeEntrySurjectionProof(uint256& entry, const uint256 &hash, const std::vector& proof, const std::vector& commitment) { CSHA256 hasher = m_salted_hasher_surjection_proof; diff --git a/src/test/blind_tests.cpp b/src/test/blind_tests.cpp index 8cb328f195..eae42e5d49 100644 --- a/src/test/blind_tests.cpp +++ b/src/test/blind_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include