Skip to main content

taproot_assets_core/verify/
group_key_reveal.rs

1//! Group key reveal verification logic.
2
3use bitcoin::hashes::Hash;
4use taproot_assets_types::asset::{
5    Asset, AssetID, GroupKeyReveal, NonSpendLeafVersion, SerializedKey,
6};
7use taproot_assets_types::proof::Proof;
8
9use crate::{OpsError, TaprootOps};
10
11/// Errors returned by group key reveal verification.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Error {
14    /// Group key reveal is present but the asset has no group key.
15    MissingGroupKey,
16    /// Group key reveal requires genesis information.
17    MissingGenesis,
18    /// Asset group key length is invalid.
19    InvalidGroupKeyLength {
20        /// Expected length in bytes.
21        expected: usize,
22        /// Actual length in bytes.
23        actual: usize,
24    },
25    /// Derived group key does not match the asset group key.
26    GroupKeyMismatch,
27    /// Missing tapscript root for a V1 group key reveal.
28    MissingTapscriptRoot {
29        /// Reveal version that requires a tapscript root.
30        version: NonSpendLeafVersion,
31    },
32    /// Taproot operation failed.
33    Ops(OpsError),
34}
35
36impl From<OpsError> for Error {
37    /// Converts an ops error into a group key reveal error.
38    fn from(err: OpsError) -> Self {
39        Self::Ops(err)
40    }
41}
42
43impl core::fmt::Display for Error {
44    /// Formats the error for display.
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        match self {
47            Error::MissingGroupKey => {
48                write!(f, "group key reveal present but asset has no group key")
49            }
50            Error::MissingGenesis => write!(f, "group key reveal requires genesis information"),
51            Error::InvalidGroupKeyLength { expected, actual } => write!(
52                f,
53                "asset group key length must be {}, got {}",
54                expected, actual
55            ),
56            Error::GroupKeyMismatch => write!(f, "group key reveal mismatch"),
57            Error::MissingTapscriptRoot { version } => write!(
58                f,
59                "missing tapscript root for group key reveal version {:?}",
60                version
61            ),
62            Error::Ops(err) => core::fmt::Display::fmt(err, f),
63        }
64    }
65}
66
67/// Derives the compressed group key bytes for a group key reveal.
68pub fn group_pubkey_from_reveal<O: TaprootOps>(
69    ops: &O,
70    reveal: &GroupKeyReveal,
71    asset_id: &AssetID,
72) -> Result<SerializedKey, Error> {
73    let tapscript_root = reveal.tapscript_root.map(|root| root.to_byte_array());
74    let custom_subtree_root = reveal.custom_subtree_root.map(|root| root.to_byte_array());
75
76    match reveal.version {
77        None => derive_group_pubkey_v0(ops, &reveal.raw_group_key, asset_id, tapscript_root),
78        Some(version) => derive_group_pubkey_v1(
79            ops,
80            version,
81            &reveal.raw_group_key,
82            asset_id,
83            tapscript_root,
84            custom_subtree_root,
85        ),
86    }
87}
88
89/// Verifies that the group key reveal derives the asset's group key.
90pub fn verify_group_key_reveal<O: TaprootOps>(ops: &O, proof: &Proof) -> Result<(), Error> {
91    verify_group_key_reveal_with_asset(ops, &proof.asset, proof.group_key_reveal.as_ref())
92        .map(|_| ())
93}
94
95/// Verifies that the group key reveal derives the asset's group key.
96pub fn verify_group_key_reveal_with_asset<O: TaprootOps>(
97    ops: &O,
98    asset: &Asset,
99    reveal: Option<&GroupKeyReveal>,
100) -> Result<Option<SerializedKey>, Error> {
101    let reveal = match reveal {
102        Some(reveal) => reveal,
103        None => return Ok(None),
104    };
105    let asset_group = asset.asset_group.as_ref().ok_or(Error::MissingGroupKey)?;
106    let asset_id = asset
107        .asset_genesis
108        .as_ref()
109        .ok_or(Error::MissingGenesis)?
110        .asset_id;
111    let expected_key = if !asset_group.tweaked_group_key.is_empty() {
112        &asset_group.tweaked_group_key
113    } else {
114        &asset_group.raw_group_key
115    };
116    if expected_key.len() != 33 {
117        return Err(Error::InvalidGroupKeyLength {
118            expected: 33,
119            actual: expected_key.len(),
120        });
121    }
122
123    let derived_key = group_pubkey_from_reveal(ops, reveal, &asset_id)?;
124    if expected_key.as_slice() != &derived_key.bytes[..] {
125        return Err(Error::GroupKeyMismatch);
126    }
127
128    Ok(Some(derived_key))
129}
130
131/// Derives the tweaked group public key for a V0 group key reveal.
132fn derive_group_pubkey_v0<O: TaprootOps>(
133    ops: &O,
134    raw_key: &SerializedKey,
135    asset_id: &AssetID,
136    tapscript_root: Option<[u8; 32]>,
137) -> Result<SerializedKey, Error> {
138    let raw_pubkey = ops.parse_group_key(raw_key)?;
139    let tweak = asset_id.to_byte_array();
140    let internal_key = ops.add_tweak(&raw_pubkey, tweak)?;
141
142    let output_key = ops.taproot_output_key(&internal_key, tapscript_root)?;
143    Ok(output_key)
144}
145
146/// Derives the tweaked group public key for a V1 group key reveal.
147fn derive_group_pubkey_v1<O: TaprootOps>(
148    ops: &O,
149    version: NonSpendLeafVersion,
150    internal_key: &SerializedKey,
151    _asset_id: &AssetID,
152    tapscript_root: Option<[u8; 32]>,
153    _custom_subtree_root: Option<[u8; 32]>,
154) -> Result<SerializedKey, Error> {
155    let root = tapscript_root.ok_or(Error::MissingTapscriptRoot { version })?;
156    let internal_pubkey = ops.parse_internal_key(internal_key)?;
157
158    let output_key = ops.taproot_output_key(&internal_pubkey, Some(root))?;
159    Ok(output_key)
160}