use crate::error::ProgramError;
use crate::simplicityhl::elements::{schnorr::XOnlyPublicKey, Address};
use crate::simplicityhl::simplicity::bitcoin::key::Parity;
use crate::simplicityhl::simplicity::bitcoin::PublicKey;
use crate::simplicityhl::simplicity::ToXOnlyPubkey;
use std::fmt::Display;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use lwk_common::Network;
use lwk_signer::bip39::rand::{thread_rng, RngCore};
use lwk_wollet::elements::hex::ToHex;
use lwk_wollet::hashes::hex::FromHex;
use lwk_wollet::hashes::{sha256, Hash, HashEngine};
#[derive(Debug, thiserror::Error)]
pub enum TaprootPubkeyGenError {
#[error("Invalid pubkey recovered: expected {expected}, got {actual}")]
InvalidPubkey { expected: String, actual: String },
#[error("Invalid address recovered: expected {expected}, got {actual}")]
InvalidAddress { expected: String, actual: String },
#[error(
"Invalid taproot pubkey gen string: expected 3 parts separated by ':', got {parts_count}"
)]
InvalidFormat { parts_count: usize },
#[error("Invalid seed length: expected 32 bytes, got {actual}")]
InvalidSeedLength { actual: usize },
#[error("Failed to parse public key: {0}")]
PublicKeyParse(#[from] simplicityhl::simplicity::bitcoin::key::ParsePublicKeyError),
#[error("Failed to parse address: {0}")]
AddressParse(#[from] simplicityhl::elements::address::AddressError),
#[error("Failed to create X-only public key from bytes: {0}")]
XOnlyPublicKey(#[from] simplicityhl::simplicity::bitcoin::secp256k1::Error),
#[error("Invalid external x-only key: {0}")]
InvalidExternalKey(String),
#[error("Failed to generate address: {0}")]
AddressGeneration(#[from] ProgramError),
#[error("hex error: {0}")]
Hex(#[from] lwk_wollet::hashes::hex::HexToBytesError),
}
pub fn generate_public_key_without_private() -> (PublicKey, Vec<u8>) {
let derived_public_key;
loop {
if let Ok(public_key) = try_generate_public_key_without_private() {
derived_public_key = public_key;
break;
}
}
derived_public_key
}
pub fn get_random_seed() -> [u8; 32] {
let mut bytes: [u8; 32] = [0; 32];
thread_rng().fill_bytes(&mut bytes);
bytes
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
enum TaprootIdentity {
Seed(Vec<u8>),
ExternalXOnly(XOnlyPublicKey),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaprootPubkeyGen {
identity: TaprootIdentity,
pub pubkey: PublicKey,
pub address: Address,
}
impl TaprootPubkeyGen {
pub fn from<A>(
arguments: &A,
network: Network,
get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result<Address, ProgramError>,
) -> Result<Self, TaprootPubkeyGenError> {
let (derived_public_key, seed) = generate_public_key_without_private();
let address = get_address(&derived_public_key.to_x_only_pubkey(), arguments, network)?;
Ok(Self {
identity: TaprootIdentity::Seed(seed),
pubkey: derived_public_key,
address,
})
}
pub fn build_from_str<A>(
s: &str,
arguments: &A,
network: Network,
get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result<Address, ProgramError>,
) -> Result<Self, TaprootPubkeyGenError> {
let taproot_pubkey_gen = Self::parse_from_str(s)?;
taproot_pubkey_gen.verify(arguments, network, get_address)?;
Ok(taproot_pubkey_gen)
}
pub fn verify<A>(
&self,
arguments: &A,
network: Network,
get_address: &impl Fn(&XOnlyPublicKey, &A, Network) -> Result<Address, ProgramError>,
) -> Result<(), TaprootPubkeyGenError> {
match &self.identity {
TaprootIdentity::Seed(seed) => {
let rand_seed = seed.as_slice();
let mut eng = sha256::Hash::engine();
eng.input(rand_seed);
eng.input(rand_seed);
eng.input(rand_seed);
let potential_pubkey: [u8; 32] = sha256::Hash::from_engine(eng).to_byte_array();
let expected_pubkey: PublicKey = XOnlyPublicKey::from_slice(&potential_pubkey)?
.public_key(Parity::Even)
.into();
if expected_pubkey != self.pubkey {
return Err(TaprootPubkeyGenError::InvalidPubkey {
expected: expected_pubkey.to_string(),
actual: self.pubkey.to_string(),
});
}
}
TaprootIdentity::ExternalXOnly(xonly) => {
if &self.pubkey.to_x_only_pubkey() != xonly {
let expected_pubkey: PublicKey = xonly.public_key(Parity::Even).into();
return Err(TaprootPubkeyGenError::InvalidPubkey {
expected: expected_pubkey.to_string(),
actual: self.pubkey.to_string(),
});
}
}
}
let expected_address = get_address(&self.pubkey.to_x_only_pubkey(), arguments, network)?;
if self.address != expected_address {
return Err(TaprootPubkeyGenError::InvalidAddress {
expected: expected_address.to_string(),
actual: self.address.to_string(),
});
}
Ok(())
}
pub fn get_x_only_pubkey(&self) -> XOnlyPublicKey {
self.pubkey.to_x_only_pubkey()
}
pub fn to_json(&self) -> serde_json::Result<Value> {
serde_json::to_value(self)
}
fn parse_from_str(s: &str) -> Result<Self, TaprootPubkeyGenError> {
let parts = s.split(':').collect::<Vec<&str>>();
if parts.len() != 3 {
return Err(TaprootPubkeyGenError::InvalidFormat {
parts_count: parts.len(),
});
}
let identity = if let Some(xonly_hex) = parts[0].strip_prefix("ext-") {
let xonly_bytes = Vec::<u8>::from_hex(xonly_hex)?;
TaprootIdentity::ExternalXOnly(
XOnlyPublicKey::from_slice(&xonly_bytes)
.map_err(|e| TaprootPubkeyGenError::InvalidExternalKey(e.to_string()))?,
)
} else {
let seed = Vec::<u8>::from_hex(parts[0])?;
if seed.len() != 32 {
return Err(TaprootPubkeyGenError::InvalidSeedLength { actual: seed.len() });
}
TaprootIdentity::Seed(seed)
};
Ok(Self {
identity,
pubkey: PublicKey::from_str(parts[1])?,
address: Address::from_str(parts[2])?,
})
}
}
impl Display for TaprootPubkeyGen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let id = match &self.identity {
TaprootIdentity::Seed(seed) => seed.to_hex(),
TaprootIdentity::ExternalXOnly(xonly) => {
format!("ext-{}", xonly.serialize().to_hex())
}
};
write!(f, "{}:{}:{}", id, self.pubkey, self.address)
}
}
fn try_generate_public_key_without_private() -> Result<(PublicKey, Vec<u8>), TaprootPubkeyGenError>
{
let rand_seed: [u8; 32] = get_random_seed();
let mut eng = sha256::Hash::engine();
eng.input(&rand_seed);
eng.input(&rand_seed);
eng.input(&rand_seed);
let potential_pubkey: [u8; 32] = sha256::Hash::from_engine(eng).to_byte_array();
Ok((
XOnlyPublicKey::from_slice(&potential_pubkey)?
.public_key(Parity::Even)
.into(),
rand_seed.to_vec(),
))
}
#[cfg(test)]
mod tests {
use super::*;
const FIXED_ADDRESS: &str =
"lq1qqvxk052kf3qtkxmrakx50a9gc3smqad2ync54hzntjt980kfej9kkfe0247rp5h4yzmdftsahhw64uy8pzfe7cpg4fgykm7cv";
const ALTERNATE_ADDRESS: &str =
"lq1qqtmf5e3g4ats3yexwdfn6kfhp9sl68kdl47g75k58rvw2w33zuarwfe0247rp5h4yzmdftsahhw64uy8pzfe7k9s63c7cku58";
fn fixed_address(_: &XOnlyPublicKey, _: &(), _: Network) -> Result<Address, ProgramError> {
Ok(Address::from_str(FIXED_ADDRESS).expect("valid fixed address"))
}
#[test]
fn roundtrip_display_and_build_from_str() {
let generated = TaprootPubkeyGen::from(&(), Network::Liquid, &fixed_address)
.expect("generate taproot handle");
let serialized = generated.to_string();
let rebuilt =
TaprootPubkeyGen::build_from_str(&serialized, &(), Network::Liquid, &fixed_address)
.expect("rebuild and verify");
assert_eq!(rebuilt, generated);
}
#[test]
fn invalid_format_is_rejected() {
let err =
TaprootPubkeyGen::build_from_str("qwerty:12345", &(), Network::Liquid, &fixed_address)
.expect_err("invalid format must fail");
assert!(matches!(
err,
TaprootPubkeyGenError::InvalidFormat { parts_count: 2 }
));
}
#[test]
fn tampered_pubkey_is_rejected() {
let generated = TaprootPubkeyGen::from(&(), Network::Liquid, &fixed_address)
.expect("generate taproot handle");
let mut parts = generated
.to_string()
.split(':')
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
parts[1] = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string();
let tampered = parts.join(":");
let err = TaprootPubkeyGen::build_from_str(&tampered, &(), Network::Liquid, &fixed_address)
.expect_err("tampered pubkey must fail");
assert!(matches!(err, TaprootPubkeyGenError::InvalidPubkey { .. }));
}
#[test]
fn tampered_address_is_rejected() {
let generated = TaprootPubkeyGen::from(&(), Network::Liquid, &fixed_address)
.expect("generate taproot handle");
let mut parts = generated
.to_string()
.split(':')
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
parts[2] = ALTERNATE_ADDRESS.to_string();
let tampered = parts.join(":");
let err = TaprootPubkeyGen::build_from_str(&tampered, &(), Network::Liquid, &fixed_address)
.expect_err("tampered address must fail");
assert!(matches!(err, TaprootPubkeyGenError::InvalidAddress { .. }));
}
}