Skip to content

Commit bc124b3

Browse files
authored
Sign and verify P2SH-P2WPKH (#32)
1 parent b0441fe commit bc124b3

File tree

6 files changed

+115
-20
lines changed

6 files changed

+115
-20
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ generic message signing and verification.
66

77
## Types of Signatures
88

9-
At the moment this crate supports ONLY `P2TR` and `P2WPKH` addresses. We're
10-
looking to stabilize the interface before implementing different address types.
11-
Feedback through issues or PRs is welcome and encouraged.
9+
At the moment this crate supports `P2TR`, `P2WPKH` and `P2SH-P2WPKH` single-sig
10+
addresses. Feedback through issues or PRs on the interface design and security
11+
is welcome and encouraged.
1212

13-
- [ ] legacy
1413
- [x] simple
1514
- [x] full
1615
- [ ] full (proof-of-funds)
16+
- [ ] legacy (BIP-137)
1717

1818
The goal is to provide a full signing and verifying library similar to
1919
[this](https://github.com/ACken2/bip322-js/tree/main) Javascript library.

src/error.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub enum Error {
1010
},
1111
#[snafu(display("Failed to parse private key"))]
1212
PrivateKeyParse { source: bitcoin::key::Error },
13-
#[snafu(display("Unsuported address `{address}`, only P2TR or P2WPKH allowed"))]
13+
#[snafu(display("Unsuported address `{address}`, only P2TR, P2WPKH and P2SH-P2WPKH allowed"))]
1414
UnsupportedAddress { address: String },
1515
#[snafu(display("Decode error for signature `{signature}`"))]
1616
SignatureDecode {

src/lib.rs

+62-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ mod tests {
4242
const WIF_PRIVATE_KEY: &str = "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k";
4343
const SEGWIT_ADDRESS: &str = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l";
4444
const TAPROOT_ADDRESS: &str = "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3";
45+
const LEGACY_ADDRESS: &str = "14vV3aCHBeStb5bkenkNHbe2YAFinYdXgc";
46+
47+
const NESTED_SEGWIT_WIF_PRIVATE_KEY: &str =
48+
"KwTbAxmBXjoZM3bzbXixEr9nxLhyYSM4vp2swet58i19bw9sqk5z";
49+
const NESTED_SEGWIT_ADDRESS: &str = "3HSVzEhCFuH9Z3wvoWTexy7BMVVp3PjS6f";
4550

4651
#[test]
4752
fn message_hashes_are_correct() {
@@ -161,10 +166,10 @@ mod tests {
161166
#[test]
162167
fn invalid_address() {
163168
assert_eq!(verify::verify_simple_encoded(
164-
"3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV",
169+
LEGACY_ADDRESS,
165170
"",
166171
"AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=").unwrap_err().to_string(),
167-
"Unsuported address `3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV`, only P2TR or P2WPKH allowed"
172+
format!("Unsuported address `{LEGACY_ADDRESS}`, only P2TR, P2WPKH and P2SH-P2WPKH allowed")
168173
)
169174
}
170175

@@ -274,4 +279,59 @@ mod tests {
274279
)
275280
.is_ok());
276281
}
282+
283+
#[test]
284+
fn simple_verify_and_falsify_p2sh_p2wpkh() {
285+
assert!(verify::verify_simple_encoded(
286+
NESTED_SEGWIT_ADDRESS,
287+
"Hello World",
288+
"AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w"
289+
).is_ok()
290+
);
291+
292+
assert!(verify::verify_simple_encoded(
293+
NESTED_SEGWIT_ADDRESS,
294+
"Hello World - this should fail",
295+
"AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w"
296+
).is_err()
297+
);
298+
}
299+
300+
#[test]
301+
fn simple_sign_p2sh_p2wpkh() {
302+
assert_eq!(
303+
sign::sign_simple_encoded(NESTED_SEGWIT_ADDRESS, "Hello World", NESTED_SEGWIT_WIF_PRIVATE_KEY).unwrap(),
304+
"AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w"
305+
);
306+
}
307+
308+
#[test]
309+
fn roundtrip_p2sh_p2wpkh_simple() {
310+
assert!(verify::verify_simple_encoded(
311+
NESTED_SEGWIT_ADDRESS,
312+
"Hello World",
313+
&sign::sign_simple_encoded(
314+
NESTED_SEGWIT_ADDRESS,
315+
"Hello World",
316+
NESTED_SEGWIT_WIF_PRIVATE_KEY
317+
)
318+
.unwrap()
319+
)
320+
.is_ok());
321+
}
322+
323+
#[test]
324+
fn roundtrip_p2sh_p2wpkh_full() {
325+
assert!(verify::verify_full_encoded(
326+
NESTED_SEGWIT_ADDRESS,
327+
"Hello World",
328+
&sign::sign_full_encoded(
329+
NESTED_SEGWIT_ADDRESS,
330+
"Hello World",
331+
NESTED_SEGWIT_WIF_PRIVATE_KEY
332+
)
333+
.unwrap()
334+
)
335+
.is_ok());
336+
}
277337
}

src/sign.rs

+19-7
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ pub fn sign_full(
5555
let to_spend = create_to_spend(address, message)?;
5656
let mut to_sign = create_to_sign(&to_spend, None)?;
5757

58-
let witness =
59-
if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() {
58+
let witness = match address.payload() {
59+
Payload::WitnessProgram(witness_program) => {
6060
let version = witness_program.version().to_num();
6161
let program_len = witness_program.program().len();
6262

@@ -65,7 +65,7 @@ pub fn sign_full(
6565
if program_len != 20 {
6666
return Err(Error::NotKeyPathSpend);
6767
}
68-
create_message_signature_p2wpkh(&to_spend, &to_sign, private_key)
68+
create_message_signature_p2wpkh(&to_spend, &to_sign, private_key, false)
6969
}
7070
1 => {
7171
if program_len != 32 {
@@ -79,11 +79,16 @@ pub fn sign_full(
7979
})
8080
}
8181
}
82-
} else {
82+
}
83+
Payload::ScriptHash(_) => {
84+
create_message_signature_p2wpkh(&to_spend, &to_sign, private_key, true)
85+
}
86+
_ => {
8387
return Err(Error::UnsupportedAddress {
8488
address: address.to_string(),
8589
});
86-
};
90+
}
91+
};
8792

8893
to_sign.inputs[0].final_script_witness = Some(witness);
8994

@@ -94,15 +99,22 @@ fn create_message_signature_p2wpkh(
9499
to_spend_tx: &Transaction,
95100
to_sign: &Psbt,
96101
private_key: PrivateKey,
102+
is_p2sh: bool,
97103
) -> Witness {
98104
let secp = Secp256k1::new();
99105
let sighash_type = EcdsaSighashType::All;
100106
let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx.clone());
101107

108+
let pub_key = private_key.public_key(&secp);
109+
102110
let sighash = sighash_cache
103111
.p2wpkh_signature_hash(
104112
0,
105-
&to_spend_tx.output[0].script_pubkey,
113+
&if is_p2sh {
114+
ScriptBuf::new_p2wpkh(&pub_key.wpubkey_hash().unwrap())
115+
} else {
116+
to_spend_tx.output[0].script_pubkey.clone()
117+
},
106118
to_spend_tx.output[0].value,
107119
sighash_type,
108120
)
@@ -126,7 +138,7 @@ fn create_message_signature_p2wpkh(
126138
.to_vec(),
127139
);
128140

129-
witness.push(private_key.public_key(&secp).to_bytes());
141+
witness.push(pub_key.to_bytes());
130142

131143
witness.to_owned()
132144
}

src/verify.rs

+23-6
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,27 @@ pub fn verify_simple(address: &Address, message: &[u8], signature: Witness) -> R
5353
/// Verifies the BIP-322 full from proper Rust types.
5454
pub fn verify_full(address: &Address, message: &[u8], to_sign: Transaction) -> Result<()> {
5555
match address.payload() {
56-
Payload::WitnessProgram(wp) if wp.version().to_num() == 0 && wp.program().len() == 20 => {
56+
Payload::WitnessProgram(witness)
57+
if witness.version().to_num() == 1 && witness.program().len() == 32 =>
58+
{
59+
let pub_key = XOnlyPublicKey::from_slice(witness.program().as_bytes())
60+
.map_err(|_| Error::InvalidPublicKey)?;
61+
62+
verify_full_p2tr(address, message, to_sign, pub_key)
63+
}
64+
Payload::WitnessProgram(witness)
65+
if witness.version().to_num() == 0 && witness.program().len() == 20 =>
66+
{
5767
let pub_key =
5868
PublicKey::from_slice(&to_sign.input[0].witness[1]).map_err(|_| Error::InvalidPublicKey)?;
59-
verify_full_p2wpkh(address, message, to_sign, pub_key)
69+
70+
verify_full_p2wpkh(address, message, to_sign, pub_key, false)
6071
}
61-
Payload::WitnessProgram(wp) if wp.version().to_num() == 1 && wp.program().len() == 32 => {
72+
Payload::ScriptHash(_) => {
6273
let pub_key =
63-
XOnlyPublicKey::from_slice(wp.program().as_bytes()).map_err(|_| Error::InvalidPublicKey)?;
64-
verify_full_p2tr(address, message, to_sign, pub_key)
74+
PublicKey::from_slice(&to_sign.input[0].witness[1]).map_err(|_| Error::InvalidPublicKey)?;
75+
76+
verify_full_p2wpkh(address, message, to_sign, pub_key, true)
6577
}
6678
_ => Err(Error::UnsupportedAddress {
6779
address: address.to_string(),
@@ -74,6 +86,7 @@ fn verify_full_p2wpkh(
7486
message: &[u8],
7587
to_sign: Transaction,
7688
pub_key: PublicKey,
89+
is_p2sh: bool,
7790
) -> Result<()> {
7891
let to_spend = create_to_spend(address, message)?;
7992
let to_sign = create_to_sign(&to_spend, Some(to_sign.input[0].witness.clone()))?;
@@ -131,7 +144,11 @@ fn verify_full_p2wpkh(
131144
let sighash = sighash_cache
132145
.p2wpkh_signature_hash(
133146
0,
134-
&to_spend.output[0].script_pubkey,
147+
&if is_p2sh {
148+
ScriptBuf::new_p2wpkh(&pub_key.wpubkey_hash().unwrap())
149+
} else {
150+
to_spend.output[0].script_pubkey.clone()
151+
},
135152
to_spend.output[0].value,
136153
sighash_type,
137154
)

www/Cargo.lock

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)