Return error for psbt if explicit amounts/assets deleted
What changed, and why it matters
This commit fixes a security bug in how the Elements wallet signs partially-signed Bitcoin transactions (PSBTs) for confidential (blinded) payments. Previously, a malicious counterparty could remove the plain-text amount and asset fields from a transaction output before asking the wallet to sign. The wallet would then unblind the output and blindly trust whatever hidden value the counterparty had committed to, potentially allowing the wallet to sign away funds without knowing the real amount or asset type. The patch now refuses to sign if those explicit fields are missing, and replaces internal 'this can never happen' crash assertions with proper error returns, because the transaction data comes from outside the wallet.
Treat this as a security fix and include it in the next release. Users handling confidential transactions with PSBTs, especially multi-party or hardware-wallet workflows, should upgrade. Review related PSBT signing paths for similar reliance on optional explicit fields.
Security signals we found
Missing input validation on attacker-controlled PSBT data
Blinding proof verification relied on explicit fields that could be omitted by a counterparty
Assertions replaced with safe error returns for off-host data
Potential for wallet to sign a transaction with attacker-chosen confidential value/asset commitments
Evidence from the diff
In CWallet::SignPSBT, when processing a blinded output owned by the wallet, the code now checks that o.amount and o.m_asset are present before attempting to sign. If either is missing, it returns the new PSBTError::MISSING_EXPLICIT_OUTPUT_DATA. The subsequent UnblindConfidentialPair success path previously used assert() to verify that the unblinded value/asset matched the explicit fields; these have been changed to return PSBTError::INVALID_VALUE_PROOF and PSBTError::INVALID_ASSET_PROOF respectively, because PSBT inputs are attacker-controlled and assertions would terminate the node rather than safely reject the input. A new error string and enum value were added to support this.
Changed components
src/wallet/wallet.cpp - CWallet::SignPSBTsrc/common/types.h - PSBTError enumsrc/common/messages.cpp - PSBTErrorStringInspect captured patch +18 / −7
### src/common/messages.cpp
@@ -130,6 +130,8 @@ bilingual_str PSBTErrorString(PSBTError err)
return Untranslated("Wallet does not have necessary blinding key");
case PSBTError::MISSING_SIDECHANNEL_DATA:
return Untranslated("A rangeproof did not encode necessary blinding data");
+ case PSBTError::MISSING_EXPLICIT_OUTPUT_DATA:
+ return Untranslated("Explicit output data is missing for a blinded output");
// no default case, so the compiler can warn about missing cases
}
assert(false);
### src/common/types.h
@@ -27,6 +27,7 @@ enum class PSBTError {
INVALID_ASSET_PROOF,
MISSING_BLINDING_KEY,
MISSING_SIDECHANNEL_DATA,
+ MISSING_EXPLICIT_OUTPUT_DATA,
};
} // namespace common
### src/wallet/wallet.cpp
@@ -2475,6 +2475,13 @@ std::optional<PSBTError> CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bo
}
if (o.script && IsMine(*o.script)) {
+ // A counterparty blinding our receive output can
+ // omit them, disabling both, and we would sign a commitment
+ // to whatever value they chose. Our own blinder always
+ // preserves these fields, so requiring them is safe.
+ if (o.amount == std::nullopt || o.m_asset.IsNull()) {
+ return PSBTError::MISSING_EXPLICIT_OUTPUT_DATA;
+ }
CKey blinding_key;
if ((blinding_key = GetBlindingKey(&*o.script)).IsValid()) {
CAmount value;
@@ -2485,14 +2492,15 @@ std::optional<PSBTError> CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bo
CConfidentialNonce nonce;
nonce.vchCommitment.insert(nonce.vchCommitment.end(), o.m_ecdh_pubkey.begin(), o.m_ecdh_pubkey.end());
if (UnblindConfidentialPair(blinding_key, o.m_value_commitment, o.m_asset_commitment, nonce, *o.script, o.m_value_rangeproof, value, value_factor, asset, asset_factor)) {
- // These assertions are cryptographically impossible to trigger, as we
- // checked the proofs above, and then `UnblindConfidentialPair` checks
- // the extracted value/asset against the commitments.
- if (o.amount) {
- assert(*o.amount == value);
+ // The explicit fields are required above, so
+ // VerifyBlindProofs has checked both proofs and
+ // these should not differ. Return rather than
+ // assert: the inputs originate off-host.
+ if (*o.amount != value) {
+ return PSBTError::INVALID_VALUE_PROOF;
}
- if (!o.m_asset.IsNull()) {
- assert(CAsset(o.m_asset) == asset);
+ if (CAsset(o.m_asset) != asset) {
+ return PSBTError::INVALID_ASSET_PROOF;
}
} else {
return PSBTError::MISSING_SIDECHANNEL_DATA;Why this scored 72/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.