Skip to content

Commit 881ca8d

Browse files
committed
[signer] Add an option to explicitly allow using non-ALL sighashes
Instead of blindly using the `sighash_type` set in a psbt input, we now only sign `SIGHASH_ALL` inputs by default, and require the user to explicitly opt-in to using other sighashes if they desire to do so. Fixes #350
1 parent 5633475 commit 881ca8d

File tree

4 files changed

+79
-3
lines changed

4 files changed

+79
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Wallet
10+
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
11+
912
## [v0.7.0] - [v0.6.0]
1013

1114
### Policy

src/psbt/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ mod test {
6363
psbt.inputs.push(psbt_bip.inputs[0].clone());
6464
let options = SignOptions {
6565
trust_witness_utxo: true,
66-
assume_height: None,
66+
..Default::default()
6767
};
6868
let _ = wallet.sign(&mut psbt, options).unwrap();
6969
}
@@ -80,7 +80,7 @@ mod test {
8080
psbt.inputs.push(psbt_bip.inputs[1].clone());
8181
let options = SignOptions {
8282
trust_witness_utxo: true,
83-
assume_height: None,
83+
..Default::default()
8484
};
8585
let _ = wallet.sign(&mut psbt, options).unwrap();
8686
}
@@ -96,7 +96,7 @@ mod test {
9696
psbt.global.unsigned_tx.input.push(TxIn::default());
9797
let options = SignOptions {
9898
trust_witness_utxo: true,
99-
assume_height: None,
99+
..Default::default()
100100
};
101101
let _ = wallet.sign(&mut psbt, options).unwrap();
102102
}

src/wallet/mod.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,17 @@ where
872872
return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo));
873873
}
874874

875+
// If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
876+
// is using `SIGHASH_ALL`
877+
if !sign_options.allow_all_sighashes
878+
&& !psbt
879+
.inputs
880+
.iter()
881+
.all(|i| i.sighash_type.is_none() || i.sighash_type == Some(SigHashType::All))
882+
{
883+
return Err(Error::Signer(signer::SignerError::NonStandardSighash));
884+
}
885+
875886
for signer in self
876887
.signers
877888
.signers()
@@ -1514,6 +1525,7 @@ pub(crate) mod test {
15141525
use crate::types::KeychainKind;
15151526

15161527
use super::*;
1528+
use crate::signer::{SignOptions, SignerError};
15171529
use crate::testutils;
15181530
use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset};
15191531

@@ -3670,6 +3682,54 @@ pub(crate) mod test {
36703682
)
36713683
}
36723684

3685+
#[test]
3686+
fn test_sign_nonstandard_sighash() {
3687+
let sighash = SigHashType::NonePlusAnyoneCanPay;
3688+
3689+
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
3690+
let addr = wallet.get_address(New).unwrap();
3691+
let mut builder = wallet.build_tx();
3692+
builder
3693+
.set_single_recipient(addr.script_pubkey())
3694+
.sighash(sighash)
3695+
.drain_wallet();
3696+
let (mut psbt, _) = builder.finish().unwrap();
3697+
3698+
let result = wallet.sign(&mut psbt, Default::default());
3699+
assert!(
3700+
result.is_err(),
3701+
"Signing should have failed because the TX uses non-standard sighashes"
3702+
);
3703+
assert!(
3704+
matches!(
3705+
result.unwrap_err(),
3706+
Error::Signer(SignerError::NonStandardSighash)
3707+
),
3708+
"Signing failed with the wrong error type"
3709+
);
3710+
3711+
// try again after opting-in
3712+
let result = wallet.sign(
3713+
&mut psbt,
3714+
SignOptions {
3715+
allow_all_sighashes: true,
3716+
..Default::default()
3717+
},
3718+
);
3719+
assert!(result.is_ok(), "Signing should have worked");
3720+
assert!(
3721+
result.unwrap(),
3722+
"Should finalize the input since we can produce signatures"
3723+
);
3724+
3725+
let extracted = psbt.extract_tx();
3726+
assert_eq!(
3727+
*extracted.input[0].witness[0].last().unwrap(),
3728+
sighash.as_u32() as u8,
3729+
"The signature should have been made with the right sighash"
3730+
);
3731+
}
3732+
36733733
#[test]
36743734
fn test_unused_address() {
36753735
let db = MemoryDatabase::new();

src/wallet/signer.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ pub enum SignerError {
147147
MissingWitnessScript,
148148
/// The fingerprint and derivation path are missing from the psbt input
149149
MissingHdKeypath,
150+
/// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
151+
/// explicitly allowed them
152+
///
153+
/// To enable signing transactions with non-standard sighashes set
154+
/// [`SignOptions::allow_all_sighashes`] to `true`.
155+
NonStandardSighash,
150156
}
151157

152158
impl fmt::Display for SignerError {
@@ -465,13 +471,20 @@ pub struct SignOptions {
465471
/// timelock height has already been reached. This option allows overriding the "current height" to let the
466472
/// wallet use timelocks in the future to spend a coin.
467473
pub assume_height: Option<u32>,
474+
475+
/// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
476+
/// what its value is
477+
///
478+
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
479+
pub allow_all_sighashes: bool,
468480
}
469481

470482
impl Default for SignOptions {
471483
fn default() -> Self {
472484
SignOptions {
473485
trust_witness_utxo: false,
474486
assume_height: None,
487+
allow_all_sighashes: false,
475488
}
476489
}
477490
}

0 commit comments

Comments
 (0)