rpc: default raw signing to rangeproof-committing sighash when dynafed active
What changed, and why it matters
This commit changes how the Elements blockchain software creates and checks digital signatures for raw transactions. Once a network upgrade called 'dynafed' is active, the software now defaults to a signature type that also commits to confidential transaction rangeproofs. It also fixes verification so signatures using that new default are accepted instead of rejected. The change is a protocol-alignment fix; it does not appear to introduce a security vulnerability, but it prevents a functional break where valid wallet signatures would be considered invalid.
Review that DefaultSighashType and DeploymentActiveAfter behave correctly at activation boundaries and during reorgs. Ensure tests cover both pre-dynafed and post-dynafed signing/verification paths, including PSBT finalization. No immediate incident response is indicated by the diff alone.
Security signals we found
New consensus-relevant sighash default gated by dynafed activation
Verification flag added to accept previously rejected signature type
Raw transaction signing behavior change for RPC callers
PSBT finalization path updated to match signing default
Evidence from the diff
The patch wires SIGHASH_RANGEPROOF defaulting through raw signing RPCs (signrawtransactionwithkey, descriptorprocesspsbt) and the SignTransaction helper when dynafed is active at the tip. It also adds SCRIPT_SIGHASH_RANGEPROOF to the verification flags in PSBTInputSignedAndVerified so that signatures produced with the new default are not rejected. Explicit user-provided sighash types are preserved. The change is defensive/consistency-oriented rather than a vulnerability fix.
Changed components
src/psbt.cppsrc/rpc/rawtransaction.cppsrc/rpc/rawtransaction_util.cppsrc/rpc/rawtransaction_util.hsignrawtransactionwithkey RPCdescriptorprocesspsbt RPCSignTransaction helperPSBT input verificationInspect captured patch +27 / −8
diff --git a/src/psbt.cpp b/src/psbt.cpp
index 0e04bc5..c0241c2 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -693,10 +693,15 @@ bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned
}
CMutableTransaction tx = psbt.GetUnsignedTx();
+ // ELEMENTS: enable SCRIPT_SIGHASH_RANGEPROOF when verifying, mirroring
+ // ProduceSignature()/DataFromTransaction(). Otherwise signatures using the
+ // SIGHASH_RANGEPROOF (0x40) bit -- which the wallet now produces by default
+ // once dynafed is active -- are rejected as "not understood" and the input
+ // never verifies, breaking finalization.
if (txdata) {
- return VerifyScript(input.final_script_sig, utxo.scriptPubKey, &input.final_script_witness, STANDARD_SCRIPT_VERIFY_FLAGS, MutableTransactionSignatureChecker{&tx, input_index, utxo.nValue, *txdata, MissingDataBehavior::FAIL});
+ return VerifyScript(input.final_script_sig, utxo.scriptPubKey, &input.final_script_witness, STANDARD_SCRIPT_VERIFY_FLAGS | SCRIPT_SIGHASH_RANGEPROOF, MutableTransactionSignatureChecker{&tx, input_index, utxo.nValue, *txdata, MissingDataBehavior::FAIL});
} else {
- return VerifyScript(input.final_script_sig, utxo.scriptPubKey, &input.final_script_witness, STANDARD_SCRIPT_VERIFY_FLAGS, MutableTransactionSignatureChecker{&tx, input_index, utxo.nValue, MissingDataBehavior::FAIL});
+ return VerifyScript(input.final_script_sig, utxo.scriptPubKey, &input.final_script_witness, STANDARD_SCRIPT_VERIFY_FLAGS | SCRIPT_SIGHASH_RANGEPROOF, MutableTransactionSignatureChecker{&tx, input_index, utxo.nValue, MissingDataBehavior::FAIL});
}
}
diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
index 1877d08..8e4f887 100644
--- a/src/rpc/rawtransaction.cpp
+++ b/src/rpc/rawtransaction.cpp
@@ -11,6 +11,7 @@
#include <consensus/amount.h>
#include <consensus/validation.h>
#include <core_io.h>
+#include <deploymentstatus.h>
#include <index/txindex.h>
#include <key_io.h>
#include <logging.h>
@@ -891,8 +892,10 @@ static RPCHelpMan signrawtransactionwithkey()
ParsePrevouts(request.params[2], &keystore, coins);
UniValue result(UniValue::VOBJ);
- auto tip = WITH_LOCK(::cs_main, return chainman.ActiveChain().Tip());
- SignTransaction(mtx, &keystore, coins, request.params[3], result, tip);
+ const auto [tip, sighash_rangeproof_active] = WITH_LOCK(::cs_main, return std::make_pair(
+ chainman.ActiveChain().Tip(),
+ DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_DYNA_FED)));
+ SignTransaction(mtx, &keystore, coins, request.params[3], result, tip, sighash_rangeproof_active);
return result;
},
};
@@ -3300,7 +3303,16 @@ RPCHelpMan descriptorprocesspsbt()
EvalDescriptorStringOrObject(descs[i], provider, /*expand_priv=*/true);
}
- int sighash_type = ParseSighashString(request.params[2]);
+ // ELEMENTS: when no sighash is specified, default to committing to
+ // rangeproofs if SIGHASH_RANGEPROOF is active at the current tip.
+ int sighash_type;
+ if (request.params[2].isNull()) {
+ ChainstateManager& chainman = EnsureAnyChainman(request.context);
+ const bool sighash_rangeproof_active = WITH_LOCK(::cs_main, return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_DYNA_FED));
+ sighash_type = DefaultSighashType(sighash_rangeproof_active);
+ } else {
+ sighash_type = ParseSighashString(request.params[2]);
+ }
bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool();
bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool();
diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp
index 5441913..0c30f61 100644
--- a/src/rpc/rawtransaction_util.cpp
+++ b/src/rpc/rawtransaction_util.cpp
@@ -608,9 +608,11 @@ bool ValidateTransactionPeginInputs(const CMutableTransaction& mtx, const CBlock
return immature_pegin;
}
-void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip)
+void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip, bool sighash_rangeproof_active)
{
- int nHashType = ParseSighashString(hashType);
+ // ELEMENTS: when no sighash is specified, default to committing to
+ // rangeproofs if SIGHASH_RANGEPROOF is active at the current tip.
+ int nHashType = hashType.isNull() ? DefaultSighashType(sighash_rangeproof_active) : ParseSighashString(hashType);
// Script verification errors
std::map<int, bilingual_str> input_errors;
diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h
index 7f8226c..4ff1b18 100644
--- a/src/rpc/rawtransaction_util.h
+++ b/src/rpc/rawtransaction_util.h
@@ -37,7 +37,7 @@ class SigningProvider;
* @param hashType The signature hash type
* @param result JSON object where signed transaction results accumulate
*/
-void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip);
+void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip, bool sighash_rangeproof_active);
void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, const std::map<int, bilingual_str>& input_errors, bool immature_pegin, UniValue& result);
/**
Why this scored 33/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.