use bitcoin::hashes::Hash as _;
use bitcoin::key::{TapTweak, TweakedPublicKey, UntweakedPublicKey, XOnlyPublicKey};
use bitcoin::opcodes::all::OP_RETURN;
use bitcoin::script::{Builder, PushBytesBuf, ScriptBuf};
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::taproot::TapNodeHash;
use sha2::{Digest, Sha256};
use crate::hash::Hash;
#[derive(Debug)]
pub struct TapretVerificationResult {
pub is_valid: bool,
pub output_key: Option<[u8; 32]>,
pub internal_key: Option<[u8; 32]>,
pub commitment_found: bool,
pub script_valid: bool,
pub error: Option<String>,
}
pub fn verify_tapret_script(
tapret_script: &ScriptBuf,
expected_commitment: Hash,
) -> TapretVerificationResult {
let script_valid = verify_tapret_script_structure(tapret_script);
if !script_valid {
return TapretVerificationResult {
is_valid: false,
output_key: None,
internal_key: None,
commitment_found: false,
script_valid: false,
error: Some("Invalid Tapret script structure".to_string()),
};
}
let commitment_found = verify_commitment_in_script(tapret_script, expected_commitment);
if !commitment_found {
return TapretVerificationResult {
is_valid: false,
output_key: None,
internal_key: None,
commitment_found: false,
script_valid: true,
error: Some("Commitment not found in Tapret script".to_string()),
};
}
TapretVerificationResult {
is_valid: true,
output_key: None,
internal_key: None,
commitment_found: true,
script_valid: true,
error: None,
}
}
pub fn verify_tapret_output_key<C: Verification>(
secp: &Secp256k1<C>,
internal_key: XOnlyPublicKey,
merkle_root: Option<[u8; 32]>,
expected_output_key: XOnlyPublicKey,
) -> bool {
let merkle_root_hash = merkle_root.map(|bytes| TapNodeHash::from_byte_array(bytes));
let (tweaked_key, _parity) = internal_key.tap_tweak(secp, merkle_root_hash);
let tweaked_xonly = tweaked_key.to_inner();
tweaked_xonly == expected_output_key
}
fn verify_tapret_script_structure(script: &ScriptBuf) -> bool {
let bytes = script.as_bytes();
if bytes.len() < 67 {
return false;
}
if bytes[0] != 0x6a {
return false;
}
if bytes[1] != 0x41 {
return false;
}
bytes.len() == 67
}
fn verify_commitment_in_script(script: &ScriptBuf, expected: Hash) -> bool {
let bytes = script.as_bytes();
if bytes.len() < 67 {
return false;
}
let commitment_offset = 2 + 33;
if bytes.len() < commitment_offset + 32 {
return false;
}
let embedded_commitment = &bytes[commitment_offset..commitment_offset + 32];
embedded_commitment == expected.as_bytes()
}
pub fn create_tapret_script(protocol_id: [u8; 32], nonce: u8, commitment: Hash) -> ScriptBuf {
let mut data = [0u8; 65];
data[..32].copy_from_slice(&protocol_id);
data[32] = nonce;
data[33..65].copy_from_slice(commitment.as_bytes());
let push_bytes = PushBytesBuf::try_from(data.to_vec()).unwrap();
Builder::new()
.push_opcode(OP_RETURN)
.push_slice(push_bytes)
.into_script()
}
pub fn compute_tap_tweak_hash(internal_key: [u8; 32], merkle_root: Option<[u8; 32]>) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(internal_key);
if let Some(root) = merkle_root {
hasher.update(root);
}
hasher.finalize().into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_tapret_script_structure() {
let commitment = Hash::new([0xAB; 32]);
let script = create_tapret_script([0x01; 32], 0x42, commitment);
assert!(verify_tapret_script_structure(&script));
}
#[test]
fn test_invalid_script_too_short() {
let short_script = ScriptBuf::from_bytes(vec![0x6a, 0x41, 0x01]);
assert!(!verify_tapret_script_structure(&short_script));
}
#[test]
fn test_invalid_script_not_op_return() {
let script = ScriptBuf::from_bytes(vec![0x00; 67]);
assert!(!verify_tapret_script_structure(&script));
}
#[test]
fn test_invalid_script_wrong_push() {
let mut bytes = vec![0x6a, 0x40]; bytes.resize(67, 0x00);
let script = ScriptBuf::from_bytes(bytes);
assert!(!verify_tapret_script_structure(&script));
}
#[test]
fn test_commitment_in_script() {
let commitment = Hash::new([0xCD; 32]);
let script = create_tapret_script([0x01; 32], 0x42, commitment);
assert!(verify_commitment_in_script(&script, commitment));
let wrong_commitment = Hash::new([0xFF; 32]);
assert!(!verify_commitment_in_script(&script, wrong_commitment));
}
#[test]
fn test_full_tapret_verification_valid() {
let commitment = Hash::new([0xAB; 32]);
let script = create_tapret_script([0x01; 32], 0x42, commitment);
let result = verify_tapret_script(&script, commitment);
assert!(result.is_valid);
assert!(result.commitment_found);
assert!(result.script_valid);
assert!(result.error.is_none());
}
#[test]
fn test_full_tapret_verification_wrong_commitment() {
let commitment = Hash::new([0xAB; 32]);
let script = create_tapret_script([0x01; 32], 0x42, commitment);
let wrong_commitment = Hash::new([0xFF; 32]);
let result = verify_tapret_script(&script, wrong_commitment);
assert!(!result.is_valid);
assert!(!result.commitment_found);
assert!(result.script_valid);
}
#[test]
fn test_tap_tweak_hash_deterministic() {
let key = [0x01; 32];
let root = Some([0x02; 32]);
let h1 = compute_tap_tweak_hash(key, root);
let h2 = compute_tap_tweak_hash(key, root);
assert_eq!(h1, h2);
}
#[test]
fn test_tap_tweak_hash_different_roots() {
let key = [0x01; 32];
let h1 = compute_tap_tweak_hash(key, Some([0x02; 32]));
let h2 = compute_tap_tweak_hash(key, Some([0x03; 32]));
assert_ne!(h1, h2);
}
#[test]
fn test_tap_tweak_hash_no_root() {
let key = [0x01; 32];
let h = compute_tap_tweak_hash(key, None);
assert_ne!(h, [0u8; 32]);
}
}