penumbra_sdk_asset/asset/
id.rs

1use crate::Value;
2use ark_ff::ToConstraintField;
3use ark_serialize::CanonicalDeserialize;
4use base64::Engine;
5use decaf377::Fq;
6use once_cell::sync::Lazy;
7use penumbra_sdk_num::Amount;
8use penumbra_sdk_proto::{penumbra::core::asset::v1 as pb, serializers::bech32str, DomainType};
9use serde::{Deserialize, Serialize};
10
11/// An identifier for an IBC asset type.
12///
13/// This is similar to, but different from, the design in [ADR001].  As in
14/// ADR001, a denomination trace is hashed to a fixed-size identifier, but
15/// unlike ADR001, we hash to a field element rather than a byte string.
16///
17/// A denomination trace looks like
18///
19/// - `denom` (native chain A asset)
20/// - `transfer/channelToA/denom` (chain B representation of chain A asset)
21/// - `transfer/channelToB/transfer/channelToA/denom` (chain C representation of chain B representation of chain A asset)
22///
23/// ADR001 defines the IBC asset ID as the SHA-256 hash of the denomination
24/// trace.  Instead, Penumbra hashes to a field element, so that asset IDs can
25/// be more easily used inside of a circuit.
26///
27/// [ADR001]:
28/// https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-001-coin-source-tracing.md
29#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[serde(try_from = "pb::AssetId", into = "pb::AssetId")]
31pub struct Id(pub Fq);
32
33impl From<Id> for pb::AssetId {
34    fn from(id: Id) -> Self {
35        pb::AssetId {
36            inner: id.0.to_bytes().to_vec(),
37            // Never produce a proto encoding with the alt string encoding.
38            alt_bech32m: String::new(),
39            // Never produce a proto encoding with the alt base denom.
40            alt_base_denom: String::new(),
41        }
42    }
43}
44
45impl TryFrom<pb::AssetId> for Id {
46    type Error = anyhow::Error;
47    fn try_from(value: pb::AssetId) -> Result<Self, Self::Error> {
48        if !value.inner.is_empty() {
49            if !value.alt_base_denom.is_empty() || !value.alt_bech32m.is_empty() {
50                anyhow::bail!(
51                    "AssetId proto has both inner and alt_bech32m or alt_base_denom fields set"
52                );
53            }
54            value.inner.as_slice().try_into()
55        } else if !value.alt_bech32m.is_empty() {
56            value.alt_bech32m.parse()
57        } else if !value.alt_base_denom.is_empty() {
58            Ok(Self::from_raw_denom(&value.alt_base_denom))
59        } else {
60            Err(anyhow::anyhow!(
61                "AssetId proto has neither inner nor alt_bech32m nor alt_base_denom fields set"
62            ))
63        }
64    }
65}
66
67impl DomainType for Id {
68    type Proto = pb::AssetId;
69}
70
71impl TryFrom<&[u8]> for Id {
72    type Error = anyhow::Error;
73
74    fn try_from(slice: &[u8]) -> Result<Id, Self::Error> {
75        Ok(Id(Fq::deserialize_compressed(slice)?))
76    }
77}
78
79impl TryFrom<[u8; 32]> for Id {
80    type Error = anyhow::Error;
81
82    fn try_from(bytes: [u8; 32]) -> Result<Id, Self::Error> {
83        Ok(Id(Fq::from_bytes_checked(&bytes).expect("convert to bytes")))
84    }
85}
86
87impl std::fmt::Debug for Id {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.write_str(&bech32str::encode(
90            &self.0.to_bytes(),
91            bech32str::asset_id::BECH32_PREFIX,
92            bech32str::Bech32m,
93        ))
94    }
95}
96
97impl std::fmt::Display for Id {
98    // IMPORTANT: Changing this is state-breaking.
99    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
100        f.write_str(&bech32str::encode(
101            &self.0.to_bytes(),
102            bech32str::asset_id::BECH32_PREFIX,
103            bech32str::Bech32m,
104        ))
105    }
106}
107
108impl std::str::FromStr for Id {
109    type Err = anyhow::Error;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        let inner = bech32str::decode(s, bech32str::asset_id::BECH32_PREFIX, bech32str::Bech32m)?;
113        pb::AssetId {
114            inner,
115            alt_bech32m: String::new(),
116            alt_base_denom: String::new(),
117        }
118        .try_into()
119    }
120}
121
122impl ToConstraintField<Fq> for Id {
123    fn to_field_elements(&self) -> Option<Vec<Fq>> {
124        let mut elements = Vec::new();
125        elements.extend_from_slice(&[self.0]);
126        Some(elements)
127    }
128}
129
130/// The domain separator used to hash asset ids to value generators.
131pub static VALUE_GENERATOR_DOMAIN_SEP: Lazy<Fq> = Lazy::new(|| {
132    Fq::from_le_bytes_mod_order(blake2b_simd::blake2b(b"penumbra.value.generator").as_bytes())
133});
134
135impl Id {
136    /// Compute the value generator for this asset, used for computing balance commitments.
137    pub fn value_generator(&self) -> decaf377::Element {
138        decaf377::Element::encode_to_curve(&poseidon377::hash_1(
139            &VALUE_GENERATOR_DOMAIN_SEP,
140            self.0,
141        ))
142    }
143
144    /// Convert the asset ID to bytes.
145    pub fn to_bytes(&self) -> [u8; 32] {
146        self.0.to_bytes()
147    }
148
149    /// Create a value of this denomination.
150    pub fn value(&self, amount: Amount) -> Value {
151        Value {
152            amount,
153            asset_id: *self,
154        }
155    }
156
157    pub(super) fn from_raw_denom(base_denom: &str) -> Self {
158        Id(Fq::from_le_bytes_mod_order(
159            // XXX choice of hash function?
160            blake2b_simd::Params::default()
161                .personal(b"Penumbra_AssetID")
162                .hash(base_denom.as_bytes())
163                .as_bytes(),
164        ))
165    }
166
167    /// Returns the base64 encoded string of the inner bytes.
168    pub fn to_base64(&self) -> String {
169        base64::engine::general_purpose::STANDARD.encode(self.to_bytes())
170    }
171}
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use hex;
176    use serde_json;
177    use std::str::FromStr;
178
179    #[test]
180    fn asset_id_encoding() {
181        let id = Id::from_raw_denom("upenumbra");
182
183        let bech32m_id = format!("{id}");
184
185        let id2 = Id::from_str(&bech32m_id).expect("can decode valid asset id");
186
187        use penumbra_sdk_proto::Message;
188
189        let proto = id.encode_to_vec();
190        let proto2 = pb::AssetId {
191            alt_bech32m: bech32m_id,
192            ..Default::default()
193        }
194        .encode_to_vec();
195        let proto3 = pb::AssetId {
196            alt_base_denom: "upenumbra".to_owned(),
197            ..Default::default()
198        }
199        .encode_to_vec();
200
201        let id3 = Id::decode(proto.as_ref()).expect("can decode valid asset id");
202        let id4 = Id::decode(proto2.as_ref()).expect("can decode valid asset id");
203        let id5 = Id::decode(proto3.as_ref()).expect("can decode valid asset id");
204
205        assert_eq!(id2, id);
206        assert_eq!(id3, id);
207        assert_eq!(id4, id);
208        assert_eq!(id5, id);
209    }
210
211    #[test]
212    fn hex_to_bech32() {
213        let hex_strings = [
214            "cc0d3c9eef0c7ff4e225eca85a3094603691d289aeaf428ab0d87319ad93a302", // USDY
215            "a7a339f42e671b2db1de226d4483d3e63036661cad1554d75f5f76fe04ec1e00", // SHITMOS
216            "29ea9c2f3371f6a487e7e95c247041f4a356f983eb064e5d2b3bcf322ca96a10", // UM
217            "76b3e4b10681358c123b381f90638476b7789040e47802de879f0fb3eedc8d0b", // USDC
218            "2923a0a87b3a2421f165cc853dbf73a9bdafb5da0d948564b6059cb0217c4407", // OSMO
219            "07ef660132a4c3235fab272d43d9b9752a8337b2d108597abffaff5f246d0f0f", // ATOM
220            "5314b33eecfd5ca2e99c0b6d1e0ccafe3d2dd581c952d814fb64fdf51f85c411", // TIA
221            "516108d0d0bba3f76e1f982d0a7cde118833307b03c0cd4ccb94e882b53c1f0f", // WBTC
222            "414e723f74bd987c02ccbc997585ed52b196e2ffe75b3793aa68cc2996626910", // allBTC
223            "bf8b035dda339b6cda8f221e79773b0fd871f27a472920f84c4aa2b4f98a700d", // allUSDT
224        ];
225
226        for hex in hex_strings {
227            let bytes = hex::decode(hex).expect("valid hex string");
228            let bytes_array: [u8; 32] = bytes.try_into().expect("hex is 32 bytes");
229
230            let id = Id::try_from(bytes_array).expect("valid asset ID bytes");
231            let bech32_str = id.to_string();
232
233            println!("Asset ID for {}:", hex);
234            println!("  Bech32:     {}", bech32_str);
235
236            // Print Proto JSON encoding
237            let proto: pb::AssetId = id.into();
238            println!("  Proto JSON: {}\n", serde_json::to_string(&proto).unwrap());
239
240            // Convert back to verify roundtrip
241            let id_decoded = Id::from_str(&bech32_str).expect("valid bech32 string");
242            assert_eq!(id, id_decoded);
243        }
244    }
245}