simplicityhl_core/
taproot_pubkey_gen.rs

1//! Ephemeral Taproot pubkey and address generator for argument-bound programs.
2//!
3//! Produces a deterministic X-only public key and corresponding address without
4//! holding a private key, based on a random seed. The resulting trio
5//! `<seed_hex>:<xonly_pubkey_hex>:<taproot_address>` can be printed and
6//! later verified with the same arguments to prevent mismatches.
7
8use std::{fmt::Display, str::FromStr};
9
10use sha2::{Digest, Sha256};
11use simplicityhl::elements::{Address, schnorr::XOnlyPublicKey};
12use simplicityhl::simplicity::ToXOnlyPubkey;
13use simplicityhl::simplicity::bitcoin::PublicKey;
14use simplicityhl::simplicity::bitcoin::key::Parity;
15use simplicityhl::simplicity::elements::AddressParams;
16
17/// Container for the seed, public key and derived address.
18#[derive(Debug, Clone)]
19pub struct TaprootPubkeyGen {
20    pub seed: Vec<u8>,
21    pub pubkey: PublicKey,
22    pub address: Address,
23}
24
25impl TaprootPubkeyGen {
26    /// Build from current process randomness and compute the address given `arguments`.
27    pub fn from<A>(
28        arguments: &A,
29        params: &'static AddressParams,
30        get_address: &impl Fn(&XOnlyPublicKey, &A, &'static AddressParams) -> anyhow::Result<Address>,
31    ) -> anyhow::Result<Self> {
32        let (not_existent_public_key, seed) = generate_public_key_without_private();
33
34        let address = get_address(
35            &not_existent_public_key.to_x_only_pubkey(),
36            arguments,
37            params,
38        )?;
39
40        Ok(Self {
41            seed,
42            pubkey: not_existent_public_key,
43            address,
44        })
45    }
46
47    /// Parse from string and verify that pubkey and address match the provided arguments.
48    pub fn build_from_str<A>(
49        s: &str,
50        arguments: &A,
51        params: &'static AddressParams,
52        get_address: &impl Fn(&XOnlyPublicKey, &A, &'static AddressParams) -> anyhow::Result<Address>,
53    ) -> anyhow::Result<Self> {
54        let taproot_pubkey_gen = Self::from_str(s)?;
55
56        taproot_pubkey_gen.verify(arguments, params, get_address)?;
57
58        Ok(taproot_pubkey_gen)
59    }
60
61    /// Verify that the stored pubkey and address are consistent with `arguments`.
62    pub fn verify<A>(
63        &self,
64        arguments: &A,
65        params: &'static AddressParams,
66        get_address: &impl Fn(&XOnlyPublicKey, &A, &'static AddressParams) -> anyhow::Result<Address>,
67    ) -> anyhow::Result<()> {
68        let rand_seed = self.seed.as_slice();
69
70        let mut hasher = Sha256::new();
71        sha2::digest::Update::update(&mut hasher, rand_seed);
72        sha2::digest::Update::update(&mut hasher, rand_seed);
73        sha2::digest::Update::update(&mut hasher, rand_seed);
74        let potential_pubkey: [u8; 32] = hasher.finalize().into();
75
76        let expected_pubkey: PublicKey = XOnlyPublicKey::from_slice(&potential_pubkey)?
77            .public_key(Parity::Even)
78            .into();
79        if expected_pubkey != self.pubkey {
80            return Err(anyhow::anyhow!("Invalid pubkey"));
81        }
82
83        if self.address != get_address(&self.pubkey.to_x_only_pubkey(), arguments, params)? {
84            return Err(anyhow::anyhow!("Invalid address"));
85        }
86
87        Ok(())
88    }
89
90    /// Parse `<seed_hex>:<pubkey>:<address>` representation.
91    fn from_str(s: &str) -> anyhow::Result<Self> {
92        let parts = s.split(':').collect::<Vec<&str>>();
93
94        if parts.len() != 3 {
95            return Err(anyhow::anyhow!("Invalid taproot pubkey gen string"));
96        }
97
98        Ok(Self {
99            seed: hex::decode(parts[0])?,
100            pubkey: PublicKey::from_str(parts[1])?,
101            address: Address::from_str(parts[2])?,
102        })
103    }
104}
105
106impl Display for TaprootPubkeyGen {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(
109            f,
110            "{}:{}:{}",
111            hex::encode(&self.seed),
112            self.pubkey,
113            self.address
114        )
115    }
116}
117
118/// Try to deterministically map a random seed into a valid X-only pubkey.
119fn try_generate_public_key_without_private() -> anyhow::Result<(PublicKey, Vec<u8>)> {
120    let rand_seed: [u8; 32] = get_random_seed();
121
122    let mut hasher = Sha256::new();
123    sha2::digest::Update::update(&mut hasher, &rand_seed);
124    sha2::digest::Update::update(&mut hasher, &rand_seed);
125    sha2::digest::Update::update(&mut hasher, &rand_seed);
126    let potential_pubkey: [u8; 32] = hasher.finalize().into();
127
128    Ok((
129        XOnlyPublicKey::from_slice(&potential_pubkey)?
130            .public_key(Parity::Even)
131            .into(),
132        rand_seed.to_vec(),
133    ))
134}
135
136/// Generate a valid ephemeral public key and its seed; repeats until valid.
137pub fn generate_public_key_without_private() -> (PublicKey, Vec<u8>) {
138    let not_existent_public_key;
139    loop {
140        if let Ok(public_key) = try_generate_public_key_without_private() {
141            not_existent_public_key = public_key;
142            break;
143        }
144    }
145
146    not_existent_public_key
147}
148
149/// System-random 32-byte seed.
150pub fn get_random_seed() -> [u8; 32] {
151    ring::rand::generate(&ring::rand::SystemRandom::new())
152        .unwrap()
153        .expose()
154}