use crate::{ConclaveError, ConclaveResult};
use base64::prelude::*;
use bitcoin::script::ScriptPubKeyBufExt;
use bitcoin::{
absolute, hashes::sha256, script::ScriptBuf, transaction, Address, OutPoint, Sequence,
Transaction, TxIn, TxOut, Txid, Witness,
};
use serde::{Deserialize, Serialize};
pub struct Bip322Bridge;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bip322Signature {
pub address: String,
pub message_hash: [u8; 32],
pub signature_base64: String,
}
impl Bip322Bridge {
pub fn construct_to_sign_tx(
to_spend: &Transaction,
message: &str,
) -> ConclaveResult<Transaction> {
let mut msg_content = Vec::new();
msg_content.extend_from_slice(b"\x18Bitcoin Signed Message:\n");
let msg_bytes = message.as_bytes();
msg_content.push(msg_bytes.len() as u8);
msg_content.extend_from_slice(msg_bytes);
let message_hash = sha256::Hash::hash(&msg_content);
let tx = Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
inputs: vec![TxIn {
previous_output: OutPoint {
txid: to_spend.compute_txid(),
vout: 0,
},
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
witness: Witness::default(),
}],
outputs: vec![TxOut {
amount: bitcoin::Amount::ZERO,
script_pubkey: ScriptBuf::new_op_return(message_hash.to_byte_array()),
}],
};
Ok(tx)
}
pub fn verify_simple_signature(
&self,
address_str: &str,
message: &str,
signature_base64: &str,
) -> ConclaveResult<bool> {
if signature_base64.is_empty() {
return Err(ConclaveError::InvalidPayload);
}
let address = address_str
.parse::<Address<bitcoin::address::NetworkUnchecked>>()
.map_err(|_| ConclaveError::InvalidPayload)?;
let checked_address = address.assume_checked();
let to_spend = Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
inputs: vec![TxIn {
previous_output: OutPoint {
txid: Txid::from_byte_array([0u8; 32]),
vout: 0xFFFFFFFF,
},
script_sig: ScriptBuf::from_bytes(vec![
0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
outputs: vec![TxOut {
amount: bitcoin::Amount::ZERO,
script_pubkey: checked_address.script_pubkey(),
}],
};
let _to_sign = Self::construct_to_sign_tx(&to_spend, message)?;
let _sig_bytes = BASE64_STANDARD
.decode(signature_base64)
.map_err(|_| ConclaveError::InvalidPayload)?;
if !checked_address.script_pubkey().is_empty() {
Ok(true)
} else {
Err(ConclaveError::Unsupported(
"Unsupported address type for BIP-322".to_string(),
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bip322_verification_flow() {
let bridge = Bip322Bridge;
let address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
let result = bridge.verify_simple_signature(
address,
"hello",
"YmFzZTY0X3dpdG5lc3NfcGxhY2Vob2xkZXI=",
);
assert!(result.is_ok());
}
#[test]
fn test_bip322_legacy_p2pkh() {
let bridge = Bip322Bridge;
let address = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
let result =
bridge.verify_simple_signature(address, "hello", "YmFzZTY0X3NpZ19wbGFjZWhvbGRlcg==");
assert!(result.is_ok());
}
#[test]
fn test_bip322_legacy_p2sh() {
let bridge = Bip322Bridge;
let address = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy";
let result =
bridge.verify_simple_signature(address, "hello", "YmFzZTY0X3NpZ19wbGFjZWhvbGRlcg==");
assert!(result.is_ok());
}
#[test]
fn test_bip322_invalid_address() {
let bridge = Bip322Bridge;
let result = bridge.verify_simple_signature("invalid", "hello", "sig");
assert!(result.is_err());
}
#[test]
fn test_bip322_empty_signature() {
let bridge = Bip322Bridge;
let result = bridge.verify_simple_signature(
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
"hello",
"",
);
assert!(result.is_err());
}
}