blindpsbt: require both range bounds to match claim in VerifyBlindValueProof
What changed, and why it matters
This commit fixes a bug in how Elements verifies confidential transaction value proofs. Previously, the software only checked the lower bound of a mathematical range proof against the amount shown in a transaction. That meant a proof could truthfully prove the hidden amount was at least the displayed value, while the actual hidden amount was larger. This could let someone understate the value of an output in a partially-signed transaction (PSET). The fix now requires both the lower and upper bounds to equal the claimed amount, turning the range proof into a proper equality proof.
Treat this as a security fix and include it in the next release. Users and integrators handling PSETs with blind value proofs should upgrade. Review any PSET workflows that rely on VerifyBlindValueProof() to ensure they now enforce equality of both range bounds. Consider whether any related proof types (e.g., asset proofs) have similar single-bound checks.
Security signals we found
Understated output values in PSET blind value proofs
Incomplete range-proof bound verification
Confidential transaction amount mismatch risk
Proof verification bypass via upper-bound omission
Evidence from the diff
In VerifyBlindValueProof(), secp256k1_rangeproof_verify() returns min_value and max_value for the committed confidential value. The original code only compared min_value to the claimed CAmount value. Because a range proof can prove [value, ∞) while committing to a larger value, this comparison was insufficient. The patch adds a check that max_value also equals value, ensuring the proven interval is exactly [value, value] and preventing committed values from exceeding the displayed amount.
Changed components
src/blindpsbt.cppVerifyBlindValueProof()PSET blind value proof verificationConfidential Assets / Elements range proofsInspect captured patch +5 / −1
### src/blindpsbt.cpp
@@ -212,7 +212,11 @@ bool VerifyBlindValueProof(CAmount value, const CConfidentialValue& conf_value,
if (secp256k1_rangeproof_verify(secp256k1_blind_context, &min_value, &max_value, &value_commit, proof.data(), proof.size(), /* extra_commit */ nullptr, /* extra_commit_len */ 0, &gen) == 0) {
return false;
}
- return min_value == (uint64_t)value;
+ // A range-membership proof is only meaningful as an equality proof if the
+ // proven interval collapses to the claimed amount. Comparing solely the
+ // lower bound would accept a proof whose committed value is larger than
+ // the displayed amount. Require both bounds to equal `value`.
+ return min_value == (uint64_t)value && max_value == (uint64_t)value;
}
BlindProofResult VerifyBlindProofs(const PSBTOutput& o) {Why this scored 74/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.