Skip to main content

taproot_assets_types/
asset.rs

1use crate::error::Error;
2use bitcoin::consensus::encode::serialize;
3use bitcoin::hashes::Hash;
4use bitcoin::hashes::sha256::Hash as Sha256Hash;
5use bitcoin::io::Read;
6use bitcoin::{BlockHash, OutPoint, Witness};
7use core::convert::TryFrom;
8
9use alloc::boxed::Box;
10use alloc::format;
11use alloc::string::{String, ToString};
12use alloc::vec::Vec;
13use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
14
15pub type AssetID = Sha256Hash;
16
17/// Serialized compressed public key bytes used as a stable map key.
18#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
19pub struct SerializedKey {
20    /// Raw compressed public key bytes.
21    pub bytes: [u8; 33],
22}
23
24impl Serialize for SerializedKey {
25    /// Serializes the key as a byte slice.
26    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
27    where
28        S: Serializer,
29    {
30        serializer.serialize_bytes(&self.bytes)
31    }
32}
33
34impl<'de> Deserialize<'de> for SerializedKey {
35    /// Deserializes the key from a byte slice.
36    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37    where
38        D: Deserializer<'de>,
39    {
40        struct BytesVisitor;
41
42        impl<'de> de::Visitor<'de> for BytesVisitor {
43            type Value = SerializedKey;
44
45            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
46                formatter.write_str("33 bytes")
47            }
48
49            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
50            where
51                E: de::Error,
52            {
53                if v.len() != 33 {
54                    return Err(E::invalid_length(v.len(), &"33 bytes"));
55                }
56                let mut array = [0u8; 33];
57                array.copy_from_slice(v);
58                Ok(SerializedKey { bytes: array })
59            }
60
61            fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
62            where
63                E: de::Error,
64            {
65                self.visit_bytes(&v)
66            }
67
68            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
69            where
70                A: de::SeqAccess<'de>,
71            {
72                let mut bytes = [0u8; 33];
73                for i in 0..33 {
74                    bytes[i] = seq
75                        .next_element()?
76                        .ok_or_else(|| de::Error::invalid_length(i, &"33 bytes"))?;
77                }
78                Ok(SerializedKey { bytes })
79            }
80        }
81
82        deserializer.deserialize_bytes(BytesVisitor)
83    }
84}
85
86#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
87#[repr(u8)]
88/// The version of the Taproot Asset.
89pub enum AssetVersion {
90    /// V0 is the default asset version. This version will include
91    /// the witness vector in the leaf for a tap commitment.
92    V0 = 0,
93    /// V1 is the asset version that leaves out the witness vector
94    /// from the MS-SMT leaf encoding.
95    V1 = 1,
96}
97
98impl AssetVersion {
99    pub(crate) fn from_u8(val: u8) -> Result<Self, Error> {
100        match val {
101            0 => Ok(AssetVersion::V0),
102            1 => Ok(AssetVersion::V1),
103            _ => Err(Error::InvalidTlvValue(
104                0,
105                format!("Unknown AssetVersion: {}", val),
106            )),
107        }
108    }
109}
110
111impl TryFrom<i32> for AssetVersion {
112    type Error = String;
113
114    fn try_from(value: i32) -> core::result::Result<Self, Self::Error> {
115        match value {
116            0 => Ok(AssetVersion::V0),
117            1 => Ok(AssetVersion::V1),
118            _ => Err(format!("Invalid AssetVersion value: {}", value)),
119        }
120    }
121}
122
123#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
124/// The type of the asset.
125pub enum AssetType {
126    ///
127    /// Indicates that an asset is capable of being split/merged, with each of the
128    /// units being fungible, even across a key asset ID boundary (assuming the
129    /// key group is the same).
130    Normal,
131    ///
132    /// Indicates that an asset is a collectible, meaning that each of the other
133    /// items under the same key group are not fully fungible with each other.
134    /// Collectibles also cannot be split or merged.
135    Collectible,
136}
137
138impl TryFrom<i32> for AssetType {
139    type Error = String;
140
141    fn try_from(value: i32) -> core::result::Result<Self, Self::Error> {
142        match value {
143            0 => Ok(AssetType::Normal),
144            1 => Ok(AssetType::Collectible),
145            _ => Err(format!("Invalid AssetType value: {}", value)),
146        }
147    }
148}
149
150#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
151/// The type of the script key.
152pub enum ScriptKeyType {
153    ///
154    /// The type of script key is not known. This should only be stored for assets
155    /// where we don't know the internal key of the script key (e.g. for
156    /// imported proofs).
157    Unknown,
158    ///
159    /// The script key is a normal BIP-86 key. This means that the internal key is
160    /// turned into a Taproot output key by applying a BIP-86 tweak to it.
161    Bip86,
162    ///
163    /// The script key is a key that contains a script path that is defined by the
164    /// user and is therefore external to the tapd wallet. Spending this key
165    /// requires providing a specific witness and must be signed through the vPSBT
166    /// signing flow.
167    ScriptPathExternal,
168    ///
169    /// The script key is a specific un-spendable key that indicates a burnt asset.
170    /// Assets with this key type can never be spent again, as a burn key is a
171    /// tweaked NUMS key that nobody knows the private key for.
172    Burn,
173    ///
174    /// The script key is a specific un-spendable key that indicates a tombstone
175    /// output. This is only the case for zero-value assets that result from a
176    /// non-interactive (TAP address) send where no change was left over.
177    Tombstone,
178    ///
179    /// The script key is used for an asset that resides within a Taproot Asset
180    /// Channel. That means the script key is either a funding key (OP_TRUE), a
181    /// commitment output key (to_local, to_remote, htlc), or a HTLC second-level
182    /// transaction output key. Keys related to channels are not shown in asset
183    /// balances (unless specifically requested) and are never used for coin
184    /// selection.
185    Channel,
186}
187
188impl TryFrom<i32> for ScriptKeyType {
189    type Error = String;
190
191    fn try_from(value: i32) -> core::result::Result<Self, Self::Error> {
192        match value {
193            0 => Ok(ScriptKeyType::Unknown),
194            1 => Ok(ScriptKeyType::Bip86),
195            2 => Ok(ScriptKeyType::ScriptPathExternal),
196            3 => Ok(ScriptKeyType::Burn),
197            4 => Ok(ScriptKeyType::Tombstone),
198            5 => Ok(ScriptKeyType::Channel),
199            _ => Err(format!("Invalid ScriptKeyType value: {}", value)),
200        }
201    }
202}
203
204#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
205/// Base genesis information for an asset.
206pub struct GenesisInfo {
207    /// The first outpoint of the transaction that created the asset (txid:vout).
208    pub genesis_point: OutPoint,
209    /// The name of the asset.
210    pub name: String,
211    /// The hash of the meta data for this genesis asset.
212    pub meta_hash: Sha256Hash,
213    /// The asset ID that uniquely identifies the asset.
214    pub asset_id: AssetID,
215    /// The type of the asset.
216    pub asset_type: AssetType,
217    ///
218    /// The index of the output that carries the unique Taproot Asset commitment in
219    /// the genesis transaction.
220    pub output_index: u32,
221}
222
223#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
224/// Information related to the key group of an asset (if it exists).
225pub struct AssetGroup {
226    /// The raw group key which is a normal public key.
227    pub raw_group_key: Vec<u8>,
228    ///
229    /// The tweaked group key, which is derived based on the genesis point and also
230    /// asset type.
231    pub tweaked_group_key: Vec<u8>,
232    ///
233    /// A witness that authorizes a specific asset to be part of the asset group
234    /// specified by the above key.
235    pub asset_witness: Vec<u8>,
236    ///
237    /// The root hash of a tapscript tree, which enables future issuance authorized
238    /// with a script witness.
239    pub tapscript_root: Option<Sha256Hash>,
240}
241
242#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
243/// Describes where in the chain an asset is currently anchored.
244pub struct AnchorInfo {
245    /// The transaction that anchors the Taproot Asset commitment where the asset
246    ///   resides.
247    pub anchor_tx: bitcoin::Transaction,
248    /// The hash of the block which contains the anchor transaction above.
249    pub anchor_block_hash: BlockHash,
250    /// The outpoint (txid:vout) that stores the Taproot Asset commitment.
251    pub anchor_outpoint: OutPoint,
252    ///
253    /// The raw internal key that was used to create the anchor Taproot output key.
254    pub internal_key: Vec<u8>,
255    ///
256    /// The Taproot merkle root hash of the anchor output the asset was committed
257    /// to. If there is no Tapscript sibling, this is equal to the Taproot Asset
258    /// root commitment hash.
259    pub merkle_root: Sha256Hash,
260    ///
261    /// The serialized preimage of a Tapscript sibling, if there was one. If this
262    /// is empty, then the merkle_root hash is equal to the Taproot root hash of the
263    /// anchor output.
264    pub tapscript_sibling: Vec<u8>,
265    /// The height of the block which contains the anchor transaction.
266    pub block_height: u32,
267    /// The UTC Unix timestamp of the block containing the anchor transaction.
268    pub block_timestamp: i64,
269}
270
271#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
272/// Represents a previous input asset.
273pub struct PrevInputAsset {
274    /// The previous input's anchor point (txid:vout).
275    pub anchor_point: OutPoint,
276    /// The asset ID of the asset that was spent as an input.
277    pub asset_id: AssetID,
278    /// The script key of the asset that was spent as an input.
279    pub script_key: Vec<u8>,
280    /// The amount of the asset that was spent as an input.
281    pub amount: u64,
282}
283
284#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
285/// Reference to an asset's previous input.
286pub struct PrevId {
287    /// The Bitcoin-level outpoint anchoring the asset state.
288    pub out_point: OutPoint,
289    /// The TAP-level asset identifier.
290    pub asset_id: AssetID,
291    /// The TAP-level serialized script key that committed to the asset.
292    pub script_key: SerializedKey,
293}
294
295#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
296/// Decimal display dictates how to display the amount of an asset.
297pub struct DecimalDisplay {
298    ///
299    /// Decimal display dictates the number of decimal places to shift the amount to
300    /// the left converting from Taproot Asset integer representation to a
301    /// UX-recognizable fractional quantity.
302    ///
303    /// For example, if the decimal_display value is 2 and there's 100 of those
304    /// assets, then a wallet would display the amount as "1.00". This field is
305    /// intended as information for wallets that display balances and has no impact
306    /// on the behavior of the daemon or any other part of the protocol. This value
307    /// is encoded in the MetaData field as a JSON field, therefore it is only
308    /// compatible with assets that have a JSON MetaData field.
309    pub decimal_display: u32,
310}
311
312#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
313/// Represents a Taproot Asset.
314pub struct Asset {
315    /// The version of the Taproot Asset.
316    pub version: AssetVersion,
317    /// The base genesis information of an asset. This information never changes.
318    pub asset_genesis: Option<GenesisInfo>,
319    /// The total amount of the asset stored in this Taproot Asset UTXO.
320    pub amount: u64,
321    /// An optional locktime, as with Bitcoin transactions.
322    pub lock_time: i32,
323    /// An optional relative lock time, same as Bitcoin transactions.
324    pub relative_lock_time: i32,
325    /// The version of the script, only version 0 is defined at present.
326    pub script_version: i32,
327    /// The script key of the asset, which can be spent under Taproot semantics.
328    pub script_key: Vec<u8>,
329    /// Indicates whether the script key is known to the wallet of the lnd node
330    /// connected to the Taproot Asset daemon.
331    pub script_key_is_local: bool,
332    /// The information related to the key group of an asset (if it exists).
333    pub asset_group: Option<AssetGroup>,
334    /// Describes where in the chain the asset is currently anchored.
335    pub chain_anchor: Option<AnchorInfo>,
336    /// Previous witnesses for the asset.
337    pub prev_witnesses: Vec<PrevWitness>,
338    /// The root node of the MS-SMT storing split commitments, if present.
339    pub split_commitment_root: Option<crate::mssmt::MssmtNode>,
340    /// Indicates whether the asset has been spent.
341    pub is_spent: bool,
342    /// If the asset has been leased, this is the owner (application ID) of the
343    /// lease.
344    pub lease_owner: Vec<u8>,
345    /// If the asset has been leased, this is the expiry of the lease as a Unix
346    /// timestamp in seconds.
347    pub lease_expiry: i64,
348    /// Indicates whether this transfer was an asset burn. If true, the number of
349    /// assets in this output are destroyed and can no longer be spent.
350    pub is_burn: bool,
351    /// Deprecated, use script_key_type instead!
352    /// Indicates whether this script key has either been derived by the local
353    /// wallet or was explicitly declared to be known by using the
354    /// DeclareScriptKey RPC. Knowing the key conceptually means the key belongs
355    /// to the local wallet or is at least known by a software that operates on
356    /// the local wallet. The flag is never serialized in proofs, so this is
357    /// never explicitly set for keys foreign to the local wallet. Therefore, if
358    /// this method returns true for a script key, it means the asset with the
359    /// script key will be shown in the wallet balance.
360    pub script_key_declared_known: bool,
361    /// Deprecated, use script_key_type instead!
362    /// Indicates whether the script key is known to have a Tapscript spend path,
363    /// meaning that the Taproot merkle root tweak is not empty. This will only
364    /// ever be true if either script_key_is_local or script_key_internals_known
365    /// is true as well, since the presence of a Tapscript spend path cannot be
366    /// determined for script keys that aren't known to the wallet of the local
367    /// tapd node.
368    pub script_key_has_script_path: bool,
369    /// This field defines a decimal display value that may be present. If this
370    /// field is null, it means the presence of a decimal display field is
371    /// unknown in the current context.
372    pub decimal_display: Option<DecimalDisplay>,
373    /// The type of the script key. This type is either user-declared when custom
374    /// script keys are added, or automatically determined by the daemon for
375    /// standard operations (e.g. BIP-86 keys, burn keys, tombstone keys, channel
376    /// related keys).
377    pub script_key_type: ScriptKeyType,
378}
379
380#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
381pub enum AssetMetaType {
382    Opaque,
383    Json,
384}
385
386impl TryFrom<i32> for AssetMetaType {
387    type Error = String;
388
389    fn try_from(value: i32) -> core::result::Result<Self, Self::Error> {
390        match value {
391            0 => Ok(AssetMetaType::Opaque),
392            1 => Ok(AssetMetaType::Json),
393            _ => Err(format!("Invalid AssetMetaType value: {}", value)),
394        }
395    }
396}
397
398#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
399pub struct AssetMeta {
400    pub data: Vec<u8>,
401    pub meta_type: AssetMetaType,
402}
403
404#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
405pub struct GenesisReveal {
406    pub genesis_base: Option<GenesisInfo>,
407    pub asset_type: AssetType,
408    pub amount: u64,
409    pub meta_reveal: Option<AssetMeta>,
410}
411
412#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
413#[repr(u8)]
414/// The version of the group key reveal non-spendable leaf.
415pub enum NonSpendLeafVersion {
416    /// Version that commits via an OP_RETURN.
417    OpReturn = 1,
418    /// Version that commits via a Pedersen commitment.
419    Pedersen = 2,
420}
421
422impl NonSpendLeafVersion {
423    /// Parses a raw version byte into a `NonSpendLeafVersion`.
424    pub(crate) fn from_u8(value: u8) -> Result<Self, Error> {
425        match value {
426            1 => Ok(NonSpendLeafVersion::OpReturn),
427            2 => Ok(NonSpendLeafVersion::Pedersen),
428            _ => Err(Error::InvalidTlvValue(
429                0,
430                format!("Unknown NonSpendLeafVersion: {}", value),
431            )),
432        }
433    }
434}
435
436#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
437pub struct GroupKeyReveal {
438    /// Raw group key bytes (internal key for V1 reveals).
439    pub raw_group_key: SerializedKey,
440    /// Tapscript root for the group key reveal, if present.
441    pub tapscript_root: Option<Sha256Hash>,
442    /// Optional group key reveal version (V1 only).
443    pub version: Option<NonSpendLeafVersion>,
444    /// Optional custom tapscript subtree root (V1 only).
445    pub custom_subtree_root: Option<Sha256Hash>,
446}
447
448/// TLV type for the group key reveal version field.
449const GKR_VERSION_TYPE: crate::tlv::Type = crate::tlv::Type(0);
450/// TLV type for the group key reveal internal key field.
451const GKR_INTERNAL_KEY_TYPE: crate::tlv::Type = crate::tlv::Type(2);
452/// TLV type for the group key reveal tapscript root field.
453const GKR_TAPSCRIPT_ROOT_TYPE: crate::tlv::Type = crate::tlv::Type(4);
454/// TLV type for the group key reveal custom subtree root field.
455const GKR_CUSTOM_SUBTREE_ROOT_TYPE: crate::tlv::Type = crate::tlv::Type(7);
456/// Maximum total length for a V0 group key reveal payload.
457const GROUP_KEY_REVEAL_V0_MAX_LEN: u64 = 33 + 32;
458/// Length in bytes of a serialized group internal key.
459const GROUP_KEY_REVEAL_INTERNAL_KEY_LEN: usize = 33;
460/// Length in bytes of a tapscript root hash.
461const GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN: usize = 32;
462
463/// Decodes a group key reveal payload into a `GroupKeyReveal`.
464pub(crate) fn decode_group_key_reveal(bytes: &[u8]) -> Result<GroupKeyReveal, Error> {
465    let len = bytes.len() as u64;
466    if len <= GROUP_KEY_REVEAL_V0_MAX_LEN {
467        return decode_group_key_reveal_v0(bytes);
468    }
469
470    decode_group_key_reveal_v1(bytes)
471}
472
473/// Decodes a V0 group key reveal payload.
474fn decode_group_key_reveal_v0(bytes: &[u8]) -> Result<GroupKeyReveal, Error> {
475    let len = bytes.len() as u64;
476    if len > GROUP_KEY_REVEAL_V0_MAX_LEN {
477        return Err(Error::InvalidTlvValue(
478            0,
479            format!("GroupKeyRevealV0 too large: {}", len),
480        ));
481    }
482    if len < GROUP_KEY_REVEAL_INTERNAL_KEY_LEN as u64 {
483        return Err(Error::InvalidTlvValue(
484            0,
485            format!("GroupKeyRevealV0 too short: {}", len),
486        ));
487    }
488
489    let mut cursor = bitcoin::io::Cursor::new(bytes);
490    let mut key_bytes = [0u8; GROUP_KEY_REVEAL_INTERNAL_KEY_LEN];
491    cursor.read_exact(&mut key_bytes).map_err(Error::Io)?;
492    let raw_key = SerializedKey { bytes: key_bytes };
493
494    let remaining = len - GROUP_KEY_REVEAL_INTERNAL_KEY_LEN as u64;
495    let tapscript_root_bytes = read_inline_var_bytes(&mut cursor, remaining)?;
496    let tapscript_root = match tapscript_root_bytes.len() {
497        0 => None,
498        GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN => {
499            let mut root_bytes = [0u8; GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN];
500            root_bytes.copy_from_slice(&tapscript_root_bytes);
501            Some(Sha256Hash::from_byte_array(root_bytes))
502        }
503        other => {
504            return Err(Error::InvalidTlvValue(
505                0,
506                format!("Invalid V0 tapscript root length: {}", other),
507            ));
508        }
509    };
510
511    Ok(GroupKeyReveal {
512        raw_group_key: raw_key,
513        tapscript_root,
514        version: None,
515        custom_subtree_root: None,
516    })
517}
518
519/// Decodes a V1 group key reveal payload.
520fn decode_group_key_reveal_v1(bytes: &[u8]) -> Result<GroupKeyReveal, Error> {
521    let mut stream = crate::tlv::Stream::new(bitcoin::io::Cursor::new(bytes));
522    let mut version: Option<NonSpendLeafVersion> = None;
523    let mut internal_key: Option<SerializedKey> = None;
524    let mut tapscript_root: Option<Sha256Hash> = None;
525    let mut custom_subtree_root: Option<Sha256Hash> = None;
526
527    while let Some(record) = stream.next_record().map_err(Error::TlvStream)? {
528        match record.tlv_type() {
529            GKR_VERSION_TYPE => {
530                if record.value().len() != 1 {
531                    return Err(Error::InvalidTlvValue(
532                        GKR_VERSION_TYPE.0,
533                        "Length must be 1 for group key reveal version".to_string(),
534                    ));
535                }
536                version = Some(NonSpendLeafVersion::from_u8(record.value()[0])?);
537            }
538            GKR_INTERNAL_KEY_TYPE => {
539                if record.value().len() != GROUP_KEY_REVEAL_INTERNAL_KEY_LEN {
540                    return Err(Error::InvalidTlvValue(
541                        GKR_INTERNAL_KEY_TYPE.0,
542                        "Invalid internal key length".to_string(),
543                    ));
544                }
545                let mut key_bytes = [0u8; GROUP_KEY_REVEAL_INTERNAL_KEY_LEN];
546                key_bytes.copy_from_slice(record.value());
547                internal_key = Some(SerializedKey { bytes: key_bytes });
548            }
549            GKR_TAPSCRIPT_ROOT_TYPE => {
550                if record.value().len() != GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN {
551                    return Err(Error::InvalidTlvValue(
552                        GKR_TAPSCRIPT_ROOT_TYPE.0,
553                        "Invalid tapscript root length".to_string(),
554                    ));
555                }
556                let mut root_bytes = [0u8; GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN];
557                root_bytes.copy_from_slice(record.value());
558                tapscript_root = Some(Sha256Hash::from_byte_array(root_bytes));
559            }
560            GKR_CUSTOM_SUBTREE_ROOT_TYPE => {
561                if record.value().len() != GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN {
562                    return Err(Error::InvalidTlvValue(
563                        GKR_CUSTOM_SUBTREE_ROOT_TYPE.0,
564                        "Invalid custom subtree root length".to_string(),
565                    ));
566                }
567                let mut root_bytes = [0u8; GROUP_KEY_REVEAL_TAPSCRIPT_ROOT_LEN];
568                root_bytes.copy_from_slice(record.value());
569                custom_subtree_root = Some(Sha256Hash::from_byte_array(root_bytes));
570            }
571            type_val => {
572                if !type_val.is_odd() {
573                    return Err(Error::UnknownTlvType(type_val.0));
574                }
575            }
576        }
577    }
578
579    Ok(GroupKeyReveal {
580        raw_group_key: internal_key.ok_or(Error::MissingTlvField(
581            "GroupKeyReveal.internal_key".to_string(),
582        ))?,
583        tapscript_root: Some(tapscript_root.ok_or(Error::MissingTlvField(
584            "GroupKeyReveal.tapscript_root".to_string(),
585        ))?),
586        version: Some(version.ok_or(Error::MissingTlvField("GroupKeyReveal.version".to_string()))?),
587        custom_subtree_root,
588    })
589}
590
591/// Decodes an alt leaf asset by prepending a V0 version record.
592pub(crate) fn decode_alt_leaf(bytes: &[u8]) -> Result<Asset, Error> {
593    let mut prefixed = Vec::with_capacity(bytes.len() + 3);
594    prefixed.extend_from_slice(&[0u8, 1u8, 0u8]);
595    prefixed.extend_from_slice(bytes);
596    Asset::decode_tlv(bitcoin::io::Cursor::new(&prefixed))
597}
598
599#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
600/// Represents a commitment to a split of an asset.
601pub struct SplitCommitment {
602    /// The proof for the split commitment.
603    pub proof: crate::mssmt::MssmtProof,
604    /// The root asset of the split commitment.
605    pub root_asset: Box<Asset>,
606}
607
608#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
609/// Represents a previous witness.
610pub struct PrevWitness {
611    /// Previous input asset ID.
612    pub prev_id: Option<PrevId>,
613    /// Transaction witness.
614    pub tx_witness: Witness,
615    /// Split commitment.
616    pub split_commitment: Option<SplitCommitment>,
617}
618
619// TLV Types for Asset (based on Go's asset/records.go)
620const ASSET_LEAF_VERSION: crate::tlv::Type = crate::tlv::Type(0);
621const ASSET_LEAF_GENESIS: crate::tlv::Type = crate::tlv::Type(2);
622const ASSET_LEAF_AMOUNT: crate::tlv::Type = crate::tlv::Type(6);
623const ASSET_LEAF_LOCK_TIME: crate::tlv::Type = crate::tlv::Type(7);
624const ASSET_LEAF_RELATIVE_LOCK_TIME: crate::tlv::Type = crate::tlv::Type(9);
625const ASSET_LEAF_PREV_WITNESS: crate::tlv::Type = crate::tlv::Type(11);
626/// TLV type for the split commitment root field.
627const ASSET_LEAF_SPLIT_COMMITMENT_ROOT: crate::tlv::Type = crate::tlv::Type(13);
628const ASSET_LEAF_SCRIPT_VERSION: crate::tlv::Type = crate::tlv::Type(14);
629const ASSET_LEAF_SCRIPT_KEY: crate::tlv::Type = crate::tlv::Type(16);
630const ASSET_LEAF_GROUP_KEY: crate::tlv::Type = crate::tlv::Type(17);
631const ASSET_LEAF_TYPE: crate::tlv::Type = crate::tlv::Type(4);
632
633/// TLV type for the witness prev ID field.
634const WITNESS_PREV_ID: crate::tlv::Type = crate::tlv::Type(1);
635/// TLV type for the witness stack field.
636const WITNESS_TX_WITNESS: crate::tlv::Type = crate::tlv::Type(3);
637/// TLV type for the split commitment proof field.
638const WITNESS_SPLIT_COMMITMENT: crate::tlv::Type = crate::tlv::Type(5);
639
640/// Maximum byte length of an asset's name.
641const MAX_ASSET_NAME_LENGTH: u64 = 64;
642/// Maximum number of witness stack elements we accept.
643const MAX_WITNESS_STACK_ITEMS: u64 = u16::MAX as u64;
644/// Maximum size for a single witness stack element.
645const MAX_WITNESS_ELEMENT_SIZE: u64 = u16::MAX as u64;
646
647/// Reads an inline var-bytes field with an explicit maximum length.
648fn read_inline_var_bytes<R: bitcoin::io::Read>(
649    r: &mut R,
650    max_len: u64,
651) -> Result<Vec<u8>, crate::error::Error> {
652    let len = Asset::read_varint(r)?;
653    if len > max_len {
654        return Err(crate::error::Error::InvalidTlvValue(
655            0,
656            format!("inline var bytes too large: {} (max: {})", len, max_len),
657        ));
658    }
659
660    let mut bytes = alloc::vec![0u8; len as usize];
661    r.read_exact(&mut bytes).map_err(crate::error::Error::Io)?;
662    Ok(bytes)
663}
664
665/// Decodes a Bitcoin outpoint from a TLV stream.
666fn decode_out_point<R: bitcoin::io::Read>(r: &mut R) -> Result<OutPoint, crate::error::Error> {
667    let mut hash_bytes = [0u8; 32];
668    r.read_exact(&mut hash_bytes)
669        .map_err(crate::error::Error::Io)?;
670    let mut index_bytes = [0u8; 4];
671    r.read_exact(&mut index_bytes)
672        .map_err(crate::error::Error::Io)?;
673
674    Ok(OutPoint {
675        txid: bitcoin::Txid::from_byte_array(hash_bytes),
676        vout: u32::from_be_bytes(index_bytes),
677    })
678}
679
680/// Maps an asset type to its protocol byte representation.
681fn asset_type_byte(asset_type: AssetType) -> u8 {
682    match asset_type {
683        AssetType::Normal => 0,
684        AssetType::Collectible => 1,
685    }
686}
687
688/// Computes an asset ID from genesis information.
689fn compute_asset_id(genesis: &GenesisInfo) -> AssetID {
690    let outpoint_bytes = serialize(&genesis.genesis_point);
691    let tag_hash = Sha256Hash::hash(genesis.name.as_bytes());
692
693    let mut buf = Vec::with_capacity(outpoint_bytes.len() + 32 + 32 + 4 + 1);
694    buf.extend_from_slice(&outpoint_bytes);
695    buf.extend_from_slice(&tag_hash.to_byte_array());
696    buf.extend_from_slice(&genesis.meta_hash.to_byte_array());
697    buf.extend_from_slice(&genesis.output_index.to_be_bytes());
698    buf.push(asset_type_byte(genesis.asset_type));
699
700    Sha256Hash::hash(&buf)
701}
702
703/// Decodes an asset genesis record from a TLV stream.
704pub(crate) fn decode_genesis_info<R: bitcoin::io::Read>(
705    mut r: R,
706) -> Result<GenesisInfo, crate::error::Error> {
707    let genesis_point = decode_out_point(&mut r)?;
708    let tag_bytes = read_inline_var_bytes(&mut r, MAX_ASSET_NAME_LENGTH)?;
709    let name = String::from_utf8(tag_bytes).map_err(|_| {
710        crate::error::Error::InvalidTlvValue(ASSET_LEAF_GENESIS.0, "invalid UTF-8 tag".to_string())
711    })?;
712
713    let mut meta_hash_bytes = [0u8; 32];
714    r.read_exact(&mut meta_hash_bytes)
715        .map_err(crate::error::Error::Io)?;
716    let meta_hash = Sha256Hash::from_byte_array(meta_hash_bytes);
717
718    let mut output_index_bytes = [0u8; 4];
719    r.read_exact(&mut output_index_bytes)
720        .map_err(crate::error::Error::Io)?;
721    let output_index = u32::from_be_bytes(output_index_bytes);
722
723    let mut asset_type_byte = [0u8; 1];
724    r.read_exact(&mut asset_type_byte)
725        .map_err(crate::error::Error::Io)?;
726    let asset_type = match asset_type_byte[0] {
727        0 => AssetType::Normal,
728        1 => AssetType::Collectible,
729        other => {
730            return Err(crate::error::Error::InvalidTlvValue(
731                ASSET_LEAF_TYPE.0,
732                format!("Unknown asset type: {}", other),
733            ));
734        }
735    };
736
737    let mut genesis = GenesisInfo {
738        genesis_point,
739        name,
740        meta_hash,
741        asset_id: AssetID::from_byte_array([0u8; 32]),
742        asset_type,
743        output_index,
744    };
745    genesis.asset_id = compute_asset_id(&genesis);
746
747    Ok(genesis)
748}
749
750/// Decodes a prev ID from a witness record.
751fn decode_prev_id<R: bitcoin::io::Read>(mut r: R) -> Result<PrevId, crate::error::Error> {
752    let out_point = decode_out_point(&mut r)?;
753
754    let mut asset_id_bytes = [0u8; 32];
755    r.read_exact(&mut asset_id_bytes)
756        .map_err(crate::error::Error::Io)?;
757    let asset_id = AssetID::from_byte_array(asset_id_bytes);
758
759    let mut script_key_bytes = [0u8; 33];
760    r.read_exact(&mut script_key_bytes)
761        .map_err(crate::error::Error::Io)?;
762
763    Ok(PrevId {
764        out_point,
765        asset_id,
766        script_key: SerializedKey {
767            bytes: script_key_bytes,
768        },
769    })
770}
771
772/// Decodes a witness stack from a witness record.
773fn decode_tx_witness<R: bitcoin::io::Read>(mut r: R) -> Result<Witness, crate::error::Error> {
774    let num_items = Asset::read_varint(&mut r)?;
775    if num_items > MAX_WITNESS_STACK_ITEMS {
776        return Err(crate::error::Error::InvalidTlvValue(
777            WITNESS_TX_WITNESS.0,
778            format!("too many witness items: {}", num_items),
779        ));
780    }
781
782    let mut items = Vec::with_capacity(num_items as usize);
783    for _ in 0..num_items {
784        let item = read_inline_var_bytes(&mut r, MAX_WITNESS_ELEMENT_SIZE)?;
785        items.push(item);
786    }
787
788    Ok(Witness::from(items))
789}
790
791/// Decodes a split commitment from a witness record.
792fn decode_split_commitment<R: bitcoin::io::Read>(
793    mut r: R,
794    max_len: u64,
795) -> Result<SplitCommitment, crate::error::Error> {
796    let proof_bytes = read_inline_var_bytes(&mut r, max_len)?;
797    let proof = crate::mssmt::MssmtProof::decode_tlv(bitcoin::io::Cursor::new(&proof_bytes))?;
798
799    let root_asset_bytes = read_inline_var_bytes(&mut r, max_len)?;
800    let root_asset = Asset::decode_tlv(bitcoin::io::Cursor::new(&root_asset_bytes))?;
801
802    Ok(SplitCommitment {
803        proof,
804        root_asset: Box::new(root_asset),
805    })
806}
807
808/// Decodes a split commitment root node from a leaf record.
809fn decode_split_commitment_root<R: bitcoin::io::Read>(
810    mut r: R,
811) -> Result<crate::mssmt::MssmtNode, crate::error::Error> {
812    let mut hash_bytes = [0u8; 32];
813    r.read_exact(&mut hash_bytes)
814        .map_err(crate::error::Error::Io)?;
815    let mut sum_bytes = [0u8; 8];
816    r.read_exact(&mut sum_bytes)
817        .map_err(crate::error::Error::Io)?;
818
819    Ok(crate::mssmt::MssmtNode {
820        hash: Sha256Hash::from_byte_array(hash_bytes),
821        sum: u64::from_be_bytes(sum_bytes),
822    })
823}
824
825/// Decodes a single witness from its nested TLV stream.
826fn decode_witness<R: bitcoin::io::Read>(r: R) -> Result<PrevWitness, crate::error::Error> {
827    let mut stream = crate::tlv::Stream::new(r);
828
829    let mut prev_id: Option<PrevId> = None;
830    let mut tx_witness: Option<Witness> = None;
831    let mut split_commitment: Option<SplitCommitment> = None;
832
833    while let Some(record) = stream
834        .next_record()
835        .map_err(crate::error::Error::TlvStream)?
836    {
837        match record.tlv_type() {
838            WITNESS_PREV_ID => {
839                prev_id = Some(decode_prev_id(record.value_reader())?);
840            }
841            WITNESS_TX_WITNESS => {
842                tx_witness = Some(decode_tx_witness(record.value_reader())?);
843            }
844            WITNESS_SPLIT_COMMITMENT => {
845                let max_len = record.value().len() as u64;
846                split_commitment = Some(decode_split_commitment(record.value_reader(), max_len)?);
847            }
848            type_val => {
849                if !type_val.is_odd() {
850                    return Err(crate::error::Error::UnknownTlvType(type_val.0));
851                }
852            }
853        }
854    }
855
856    Ok(PrevWitness {
857        prev_id,
858        tx_witness: tx_witness.unwrap_or_default(),
859        split_commitment,
860    })
861}
862
863/// Decodes a list of witnesses from the asset leaf record.
864fn decode_prev_witnesses<R: bitcoin::io::Read>(
865    mut r: R,
866) -> Result<Vec<PrevWitness>, crate::error::Error> {
867    let num_witnesses = Asset::read_varint(&mut r)?;
868    if num_witnesses > MAX_WITNESS_STACK_ITEMS {
869        return Err(crate::error::Error::InvalidTlvValue(
870            ASSET_LEAF_PREV_WITNESS.0,
871            format!("too many prev witnesses: {}", num_witnesses),
872        ));
873    }
874
875    let mut witnesses = Vec::with_capacity(num_witnesses as usize);
876    for _ in 0..num_witnesses {
877        let witness_bytes = read_inline_var_bytes(&mut r, MAX_WITNESS_ELEMENT_SIZE)?;
878        let witness = decode_witness(bitcoin::io::Cursor::new(&witness_bytes))?;
879        witnesses.push(witness);
880    }
881
882    Ok(witnesses)
883}
884
885impl Asset {
886    /// Decodes an Asset from a TLV byte slice.
887    pub fn decode_tlv<R: bitcoin::io::Read>(r: R) -> Result<Self, crate::error::Error> {
888        let mut stream = crate::tlv::Stream::new(r);
889        let mut version: Option<AssetVersion> = None;
890        let mut genesis: Option<GenesisInfo> = None;
891        let mut amount: Option<u64> = None;
892        let mut lock_time: Option<u64> = None;
893        let mut relative_lock_time: Option<u64> = None;
894        let mut prev_witnesses: Option<Vec<PrevWitness>> = None;
895        let mut script_version: Option<u16> = None;
896        let mut script_key: Option<Vec<u8>> = None;
897        let mut group_key: Option<AssetGroup> = None;
898        let mut unknown_odd_types = alloc::collections::BTreeMap::new();
899        let mut split_commitment_root: Option<crate::mssmt::MssmtNode> = None;
900
901        while let Some(record) = stream
902            .next_record()
903            .map_err(crate::error::Error::TlvStream)?
904        {
905            match record.tlv_type() {
906                ASSET_LEAF_VERSION => {
907                    if record.value().len() != 1 {
908                        return Err(crate::error::Error::InvalidTlvValue(
909                            ASSET_LEAF_VERSION.0,
910                            String::from("Length must be 1 for version"),
911                        ));
912                    }
913                    version = Some(AssetVersion::from_u8(record.value()[0])?);
914                }
915                ASSET_LEAF_GENESIS => {
916                    genesis = Some(decode_genesis_info(record.value_reader())?);
917                }
918                ASSET_LEAF_AMOUNT => {
919                    // Decode varint for amount
920                    let mut cursor = bitcoin::io::Cursor::new(record.value());
921                    amount = Some(Self::read_varint(&mut cursor)?);
922                }
923                ASSET_LEAF_LOCK_TIME => {
924                    // Decode varint for lock time
925                    let mut cursor = bitcoin::io::Cursor::new(record.value());
926                    let lock_time_val = Self::read_varint(&mut cursor)?;
927                    lock_time = Some(lock_time_val);
928                }
929                ASSET_LEAF_RELATIVE_LOCK_TIME => {
930                    // Decode varint for relative lock time
931                    let mut cursor = bitcoin::io::Cursor::new(record.value());
932                    let relative_lock_time_val = Self::read_varint(&mut cursor)?;
933                    relative_lock_time = Some(relative_lock_time_val);
934                }
935                ASSET_LEAF_PREV_WITNESS => {
936                    prev_witnesses = Some(decode_prev_witnesses(record.value_reader())?);
937                }
938                ASSET_LEAF_SPLIT_COMMITMENT_ROOT => {
939                    split_commitment_root =
940                        Some(decode_split_commitment_root(record.value_reader())?);
941                }
942                ASSET_LEAF_SCRIPT_VERSION => {
943                    if record.value().len() != 2 {
944                        return Err(crate::error::Error::InvalidTlvValue(
945                            ASSET_LEAF_SCRIPT_VERSION.0,
946                            String::from("Length must be 2 for script version"),
947                        ));
948                    }
949                    script_version =
950                        Some(u16::from_be_bytes([record.value()[0], record.value()[1]]));
951                }
952                ASSET_LEAF_SCRIPT_KEY => {
953                    if record.value().len() != 33 {
954                        return Err(crate::error::Error::InvalidTlvValue(
955                            ASSET_LEAF_SCRIPT_KEY.0,
956                            String::from("Length must be 33 for script key"),
957                        ));
958                    }
959                    script_key = Some(record.value().to_vec());
960                }
961                ASSET_LEAF_GROUP_KEY => {
962                    if record.value().len() != 33 {
963                        return Err(crate::error::Error::InvalidTlvValue(
964                            ASSET_LEAF_GROUP_KEY.0,
965                            String::from("Length must be 33 for group key"),
966                        ));
967                    }
968                    group_key = Some(AssetGroup {
969                        raw_group_key: record.value().to_vec(),
970                        tweaked_group_key: Vec::new(),
971                        asset_witness: Vec::new(),
972                        tapscript_root: None,
973                    });
974                }
975                ASSET_LEAF_TYPE => {
976                    if record.value().len() != 1 {
977                        return Err(crate::error::Error::InvalidTlvValue(
978                            ASSET_LEAF_TYPE.0,
979                            String::from("Length must be 1 for asset type"),
980                        ));
981                    }
982                    let _asset_type = match record.value()[0] {
983                        0 => AssetType::Normal,
984                        1 => AssetType::Collectible,
985                        _ => {
986                            return Err(crate::error::Error::InvalidTlvValue(
987                                ASSET_LEAF_TYPE.0,
988                                format!("Unknown asset type: {}", record.value()[0]),
989                            ));
990                        }
991                    };
992                }
993                type_val => {
994                    if type_val.is_odd() {
995                        unknown_odd_types.insert(type_val.0, record.value().to_vec());
996                    } else {
997                        return Err(crate::error::Error::UnknownTlvType(type_val.0));
998                    }
999                }
1000            }
1001        }
1002
1003        Ok(Asset {
1004            version: version.ok_or(crate::error::Error::MissingTlvField(
1005                "Asset.version".to_string(),
1006            ))?,
1007            asset_genesis: genesis,
1008            amount: amount.unwrap_or(0),
1009            lock_time: lock_time.map(|v| v as i32).unwrap_or(0),
1010            relative_lock_time: relative_lock_time.map(|v| v as i32).unwrap_or(0),
1011            script_version: script_version.map(|v| v as i32).unwrap_or(0),
1012            script_key: script_key.unwrap_or_default(),
1013            script_key_is_local: false, // Not encoded in TLV
1014            asset_group: group_key,
1015            chain_anchor: None, // Not encoded in TLV
1016            prev_witnesses: prev_witnesses.unwrap_or_default(),
1017            split_commitment_root,
1018            is_spent: false,                         // Not encoded in TLV
1019            lease_owner: Vec::new(),                 // Not encoded in TLV
1020            lease_expiry: 0,                         // Not encoded in TLV
1021            is_burn: false,                          // Not encoded in TLV
1022            script_key_declared_known: false,        // Not encoded in TLV
1023            script_key_has_script_path: false,       // Not encoded in TLV
1024            decimal_display: None,                   // Not encoded in TLV
1025            script_key_type: ScriptKeyType::Unknown, // Not encoded in TLV
1026        })
1027    }
1028
1029    /// Reads a variable-length integer from a reader.
1030    fn read_varint<R: bitcoin::io::Read>(r: &mut R) -> Result<u64, crate::error::Error> {
1031        let mut first_byte = [0u8; 1];
1032        r.read_exact(&mut first_byte)
1033            .map_err(crate::error::Error::Io)?;
1034
1035        match first_byte[0] {
1036            253 => {
1037                let mut u16_bytes = [0u8; 2];
1038                r.read_exact(&mut u16_bytes)
1039                    .map_err(crate::error::Error::Io)?;
1040                Ok(u16::from_be_bytes(u16_bytes) as u64)
1041            }
1042            254 => {
1043                let mut u32_bytes = [0u8; 4];
1044                r.read_exact(&mut u32_bytes)
1045                    .map_err(crate::error::Error::Io)?;
1046                Ok(u32::from_be_bytes(u32_bytes) as u64)
1047            }
1048            255 => {
1049                let mut u64_bytes = [0u8; 8];
1050                r.read_exact(&mut u64_bytes)
1051                    .map_err(crate::error::Error::Io)?;
1052                Ok(u64::from_be_bytes(u64_bytes))
1053            }
1054            _ => Ok(first_byte[0] as u64),
1055        }
1056    }
1057}