Skip to content

Commit 9abefab

Browse files
authored
Sign and verify P2WPKH (#26)
1 parent 6085a2b commit 9abefab

File tree

4 files changed

+267
-53
lines changed

4 files changed

+267
-53
lines changed

src/error.rs

+7-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 allowed"))]
13+
#[snafu(display("Unsuported address `{address}`, only P2TR or P2WPKH allowed"))]
1414
UnsupportedAddress { address: String },
1515
#[snafu(display("Decode error for signature `{signature}`"))]
1616
SignatureDecode {
@@ -60,4 +60,10 @@ pub enum Error {
6060
SigHashTypeUnsupported { sighash_type: String },
6161
#[snafu(display("Not key path spend"))]
6262
NotKeyPathSpend,
63+
#[snafu(display("Invalid public key"))]
64+
InvalidPublicKey,
65+
#[snafu(display("Invalid witness"))]
66+
InvalidWitness,
67+
#[snafu(display("Public key does not match"))]
68+
PublicKeyMismatch,
6369
}

src/lib.rs

+88-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use {
22
base64::{engine::general_purpose, Engine},
33
bitcoin::{
44
absolute::LockTime,
5-
address::AddressType,
5+
address::Payload,
66
blockdata::script,
77
consensus::Decodable,
88
consensus::Encodable,
@@ -13,7 +13,8 @@ use {
1313
secp256k1::{self, schnorr::Signature, Message, Secp256k1, XOnlyPublicKey},
1414
sighash::{self, SighashCache, TapSighashType},
1515
transaction::Version,
16-
Address, Amount, OutPoint, PrivateKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
16+
Address, Amount, EcdsaSighashType, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence,
17+
Transaction, TxIn, TxOut, Witness,
1718
},
1819
bitcoin_hashes::{sha256, Hash},
1920
error::Error,
@@ -237,7 +238,7 @@ mod tests {
237238
"3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV",
238239
"",
239240
"AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=").unwrap_err().to_string(),
240-
"Unsuported address `3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV`, only P2TR allowed"
241+
"Unsuported address `3B5fQsEXEaV8v6U3ejYc8XaKXAkyQj2MjV`, only P2TR or P2WPKH allowed"
241242
)
242243
}
243244

@@ -263,4 +264,88 @@ mod tests {
263264
"Decode error for signature `AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViH`"
264265
)
265266
}
267+
268+
#[test]
269+
fn simple_verify_and_falsify_p2wpkh() {
270+
assert!(
271+
verify_simple_encoded(
272+
SEGWIT_ADDRESS,
273+
"Hello World",
274+
"AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="
275+
).is_ok()
276+
);
277+
278+
assert!(
279+
verify_simple_encoded(
280+
SEGWIT_ADDRESS,
281+
"Hello World - this should fail",
282+
"AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="
283+
).is_err()
284+
);
285+
286+
assert!(
287+
verify_simple_encoded(
288+
SEGWIT_ADDRESS,
289+
"Hello World",
290+
"AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"
291+
).is_ok()
292+
);
293+
294+
assert!(
295+
verify_simple_encoded(
296+
SEGWIT_ADDRESS,
297+
"",
298+
"AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="
299+
).is_ok()
300+
);
301+
302+
assert!(
303+
verify_simple_encoded(
304+
SEGWIT_ADDRESS,
305+
"fail",
306+
"AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="
307+
).is_err()
308+
);
309+
310+
assert!(
311+
verify_simple_encoded(
312+
SEGWIT_ADDRESS,
313+
"",
314+
"AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"
315+
).is_ok()
316+
);
317+
}
318+
319+
#[test]
320+
fn simple_sign_p2wpkh() {
321+
assert_eq!(
322+
sign_simple_encoded(SEGWIT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap(),
323+
"AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"
324+
);
325+
326+
assert_eq!(
327+
sign_simple_encoded(SEGWIT_ADDRESS, "", WIF_PRIVATE_KEY).unwrap(),
328+
"AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"
329+
);
330+
}
331+
332+
#[test]
333+
fn roundtrip_p2wpkh_simple() {
334+
assert!(verify_simple_encoded(
335+
SEGWIT_ADDRESS,
336+
"Hello World",
337+
&sign_simple_encoded(SEGWIT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap()
338+
)
339+
.is_ok());
340+
}
341+
342+
#[test]
343+
fn roundtrip_p2wpkh_full() {
344+
assert!(verify_full_encoded(
345+
SEGWIT_ADDRESS,
346+
"Hello World",
347+
&sign_full_encoded(SEGWIT_ADDRESS, "Hello World", WIF_PRIVATE_KEY).unwrap()
348+
)
349+
.is_ok());
350+
}
266351
}

src/sign.rs

+74-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::*;
22

3-
/// Signs the BIP-322 simple from encoded values, i.e. address encoding, message string and
4-
/// WIF private key string. Returns a base64 encoded witness stack.
3+
/// Signs the BIP-322 simple from spec-compliant string encodings.
54
pub fn sign_simple_encoded(address: &str, message: &str, wif_private_key: &str) -> Result<String> {
65
let address = Address::from_str(address)
76
.context(error::AddressParse { address })?
@@ -20,8 +19,7 @@ pub fn sign_simple_encoded(address: &str, message: &str, wif_private_key: &str)
2019
Ok(general_purpose::STANDARD.encode(buffer))
2120
}
2221

23-
/// Signs the BIP-322 full from encoded values, i.e. address encoding, message string and
24-
/// WIF private key string. Returns a base64 encoded transaction.
22+
/// Signs the BIP-322 full from spec-compliant string encodings.
2523
pub fn sign_full_encoded(address: &str, message: &str, wif_private_key: &str) -> Result<String> {
2624
let address = Address::from_str(address)
2725
.context(error::AddressParse { address })?
@@ -54,32 +52,86 @@ pub fn sign_full(
5452
message: &[u8],
5553
private_key: PrivateKey,
5654
) -> Result<Transaction> {
57-
if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() {
58-
if witness_program.version().to_num() != 1 {
55+
let to_spend = create_to_spend(address, message)?;
56+
let mut to_sign = create_to_sign(&to_spend, None)?;
57+
58+
let witness =
59+
if let bitcoin::address::Payload::WitnessProgram(witness_program) = address.payload() {
60+
let version = witness_program.version().to_num();
61+
let program_len = witness_program.program().len();
62+
63+
match version {
64+
0 => {
65+
if program_len != 20 {
66+
return Err(Error::NotKeyPathSpend);
67+
}
68+
create_message_signature_p2wpkh(&to_spend, &to_sign, private_key)
69+
}
70+
1 => {
71+
if program_len != 32 {
72+
return Err(Error::NotKeyPathSpend);
73+
}
74+
create_message_signature_taproot(&to_spend, &to_sign, private_key)
75+
}
76+
_ => {
77+
return Err(Error::UnsupportedAddress {
78+
address: address.to_string(),
79+
})
80+
}
81+
}
82+
} else {
5983
return Err(Error::UnsupportedAddress {
6084
address: address.to_string(),
6185
});
62-
}
63-
64-
if witness_program.program().len() != 32 {
65-
return Err(Error::NotKeyPathSpend);
66-
}
67-
} else {
68-
return Err(Error::UnsupportedAddress {
69-
address: address.to_string(),
70-
});
71-
};
86+
};
7287

73-
let to_spend = create_to_spend(address, message)?;
74-
let mut to_sign = create_to_sign(&to_spend, None)?;
75-
76-
let witness = create_message_signature(&to_spend, &to_sign, private_key);
7788
to_sign.inputs[0].final_script_witness = Some(witness);
7889

7990
to_sign.extract_tx().context(error::TransactionExtract)
8091
}
8192

82-
fn create_message_signature(
93+
fn create_message_signature_p2wpkh(
94+
to_spend_tx: &Transaction,
95+
to_sign: &Psbt,
96+
private_key: PrivateKey,
97+
) -> Witness {
98+
let secp = Secp256k1::new();
99+
let sighash_type = EcdsaSighashType::All;
100+
let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx.clone());
101+
102+
let sighash = sighash_cache
103+
.p2wpkh_signature_hash(
104+
0,
105+
&to_spend_tx.output[0].script_pubkey,
106+
to_spend_tx.output[0].value,
107+
sighash_type,
108+
)
109+
.expect("signature hash should compute");
110+
111+
let sig = secp.sign_ecdsa(
112+
&secp256k1::Message::from_digest_slice(sighash.as_ref())
113+
.expect("should be cryptographically secure hash"),
114+
&private_key.inner,
115+
);
116+
117+
let witness = sighash_cache
118+
.witness_mut(0)
119+
.expect("getting mutable witness reference should work");
120+
121+
witness.push(
122+
bitcoin::ecdsa::Signature {
123+
sig,
124+
hash_ty: sighash_type,
125+
}
126+
.to_vec(),
127+
);
128+
129+
witness.push(private_key.public_key(&secp).to_bytes());
130+
131+
witness.to_owned()
132+
}
133+
134+
fn create_message_signature_taproot(
83135
to_spend_tx: &Transaction,
84136
to_sign: &Psbt,
85137
private_key: PrivateKey,
@@ -88,6 +140,7 @@ fn create_message_signature(
88140

89141
let secp = Secp256k1::new();
90142
let key_pair = Keypair::from_secret_key(&secp, &private_key.inner);
143+
91144
let (x_only_public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair);
92145
to_sign.inputs[0].tap_internal_key = Some(x_only_public_key);
93146

0 commit comments

Comments
 (0)