penumbra_sdk_dex/lp/
metadata.rs
1use std::num::NonZeroU32;
2
3use anyhow::Context;
4use penumbra_sdk_keys::{
5 symmetric::{POSITION_METADATA_NONCE_SIZE_BYTES, POSITION_METADATA_SIZE_BYTES},
6 PositionMetadataKey,
7};
8use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
9
10#[derive(Debug, Clone, PartialEq, Copy)]
14pub struct PositionMetadata {
15 pub strategy: NonZeroU32,
17
18 pub identifier: NonZeroU32,
20}
21
22impl PositionMetadata {
23 pub fn encrypt(
24 self,
25 pmk: &PositionMetadataKey,
26 nonce: &[u8; POSITION_METADATA_NONCE_SIZE_BYTES],
27 ) -> Vec<u8> {
28 let bytes = self.encode_to_vec();
29 let plaintext: [u8; POSITION_METADATA_SIZE_BYTES] = bytes
30 .try_into()
31 .expect("PositionMetadata MUST always be exactly POSITION_METADATA_SIZE_BYTES long");
32 pmk.encrypt(&plaintext, nonce)
33 }
34
35 pub fn decrypt(
36 pmk: &PositionMetadataKey,
37 ciphertext: Option<&[u8]>,
38 ) -> anyhow::Result<Option<Self>> {
39 let Some(ciphertext) = ciphertext else {
40 return Ok(None);
41 };
42 if ciphertext.is_empty() {
43 return Ok(None);
44 }
45 let Some(bytes) = pmk.decrypt(ciphertext) else {
46 return Ok(None);
47 };
48
49 let metadata = PositionMetadata::decode(bytes.as_slice())
50 .context("failed to decode PositionMetadata from decrypted bytes")?;
51
52 Ok(Some(metadata))
53 }
54}
55
56impl DomainType for PositionMetadata {
57 type Proto = pb::PositionMetadata;
58}
59
60impl From<PositionMetadata> for pb::PositionMetadata {
61 fn from(value: PositionMetadata) -> Self {
62 Self {
63 strategy: value.strategy.into(),
64 identifier: value.identifier.into(),
65 }
66 }
67}
68
69impl TryFrom<pb::PositionMetadata> for PositionMetadata {
70 type Error = anyhow::Error;
71
72 fn try_from(value: pb::PositionMetadata) -> Result<Self, Self::Error> {
73 Ok(Self {
74 strategy: value
75 .strategy
76 .try_into()
77 .context("strategy should be non zero")?,
78 identifier: value
79 .identifier
80 .try_into()
81 .context("identifier should be non zero")?,
82 })
83 }
84}
85
86impl Default for PositionMetadata {
87 fn default() -> Self {
88 Self {
89 strategy: NonZeroU32::new(1).expect("1 is non-zero"),
90 identifier: NonZeroU32::new(1).expect("1 is non-zero"),
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use crate::lp::PositionMetadata;
98
99 use super::pb;
100 use penumbra_sdk_keys::keys::Bip44Path;
101 use penumbra_sdk_keys::keys::SeedPhrase;
102 use penumbra_sdk_keys::keys::SpendKey;
103 use penumbra_sdk_keys::symmetric::ENCRYPTED_POSITION_METADATA_SIZE_BYTES;
104 use penumbra_sdk_keys::symmetric::POSITION_METADATA_SIZE_BYTES;
105 use penumbra_sdk_keys::PositionMetadataKey;
106 use prost::Message;
107 use rand_core::OsRng;
108 use std::num::NonZeroU32;
109
110 #[test]
111 fn encrypted_metadata_len() {
112 let posmet = PositionMetadata {
113 strategy: NonZeroU32::new(1337u32).unwrap(),
114 identifier: NonZeroU32::new(1337u32).unwrap(),
115 };
116
117 let seed_phrase = SeedPhrase::generate(OsRng);
118 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
119 let fvk_sender = sk_sender.full_viewing_key();
120
121 let proto: pb::PositionMetadata = posmet.clone().into();
122 let size = proto.encode_to_vec().len();
123 assert_eq!(size, POSITION_METADATA_SIZE_BYTES);
124
125 let pmk = PositionMetadataKey::derive(fvk_sender.outgoing());
126 let encrypted_posmet = posmet.encrypt(&pmk, &[0u8; 24]);
127 let size = encrypted_posmet.len();
128 assert_eq!(size, ENCRYPTED_POSITION_METADATA_SIZE_BYTES);
129 }
130 #[test]
131 fn encrypted_format_check() {
132 let posmet = PositionMetadata {
133 strategy: NonZeroU32::new(1337u32).unwrap(),
134 identifier: NonZeroU32::new(1337u32).unwrap(),
135 };
136
137 let seed_phrase = SeedPhrase::generate(OsRng);
138 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
139 let fvk_sender = sk_sender.full_viewing_key();
140
141 let pmk = fvk_sender.position_metadata_key();
142 let raw_nonce = [0u8; 24];
143 let encrypted_posmet = posmet.encrypt(&pmk, &raw_nonce.clone());
144 assert_eq!(
145 encrypted_posmet.len(),
146 ENCRYPTED_POSITION_METADATA_SIZE_BYTES
147 );
148
149 let nonce = encrypted_posmet[..24].to_vec();
150 assert_eq!(nonce, raw_nonce);
151 }
152
153 #[test]
154 fn encrypted_metadata_roundtrip() {
155 let posmet = PositionMetadata {
156 strategy: NonZeroU32::new(55u32).unwrap(),
157 identifier: NonZeroU32::new(1337u32).unwrap(),
158 };
159
160 let seed_phrase = SeedPhrase::generate(OsRng);
161 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
162 let fvk_sender = sk_sender.full_viewing_key();
163
164 let proto: pb::PositionMetadata = posmet.clone().into();
165 let raw_metadata = proto.encode_to_vec();
166 let size = raw_metadata.len();
167
168 assert_eq!(size, POSITION_METADATA_SIZE_BYTES);
169
170 let pmk = fvk_sender.position_metadata_key();
171 let encrypted_posmet = posmet.clone().encrypt(&pmk, &[1u8; 24]);
172 let size = encrypted_posmet.len();
173 assert_eq!(size, ENCRYPTED_POSITION_METADATA_SIZE_BYTES);
174
175 let decrypted_posmet = PositionMetadata::decrypt(&pmk, Some(&encrypted_posmet))
176 .expect("decryption should succeed")
177 .expect("decrypted metadata should not be None");
178 assert!(decrypted_posmet == posmet);
179 }
180
181 #[test]
182 fn fixed_wire_size_some_id() {
183 let posmet = PositionMetadata {
184 strategy: NonZeroU32::new(55u32).unwrap(),
185 identifier: NonZeroU32::new(1337u32).unwrap(),
186 };
187 let proto: pb::PositionMetadata = posmet.into();
188 let size = proto.encoded_len();
189 assert_eq!(size, POSITION_METADATA_SIZE_BYTES);
190 }
191
192 #[test]
193 fn fixed_wire_size_max_id() {
194 let posmet = PositionMetadata {
195 strategy: NonZeroU32::new(u32::MAX).unwrap(),
196 identifier: NonZeroU32::new(u32::MAX).unwrap(),
197 };
198 let proto: pb::PositionMetadata = posmet.into();
199 let size = proto.encoded_len();
200 assert_eq!(size, POSITION_METADATA_SIZE_BYTES);
201 }
202
203 #[test]
204 fn fixed_wire_size_max_strat_max_id() {
205 let proto = pb::PositionMetadata {
206 strategy: 127u32,
207 identifier: u32::MAX,
208 };
209 let size = proto.encoded_len();
210 assert_eq!(size, POSITION_METADATA_SIZE_BYTES);
211 }
212
213 #[test]
214 fn fixed_wire_size_small_id() {
215 let posmet = PositionMetadata {
216 strategy: NonZeroU32::new(1u32).unwrap(),
217 identifier: NonZeroU32::new(1u32).unwrap(),
218 };
219 let proto: pb::PositionMetadata = posmet.into();
220 let size = proto.encoded_len();
221 assert_eq!(size, POSITION_METADATA_SIZE_BYTES);
222 }
223
224 #[test]
225 #[should_panic]
226 fn domain_type_invalid_identifier() {
227 let proto = pb::PositionMetadata {
228 strategy: 127,
229 identifier: 0,
230 };
231 let _: PositionMetadata = proto.try_into().unwrap();
232 }
233
234 #[test]
235 fn domain_type_max_strategy() {
238 let original_proto = pb::PositionMetadata {
239 strategy: u32::MAX,
240 identifier: u32::MAX,
241 };
242 let domain: PositionMetadata = original_proto.clone().try_into().unwrap();
243
244 let expected = PositionMetadata {
245 strategy: NonZeroU32::new(u32::MAX).unwrap(),
246 identifier: NonZeroU32::new(u32::MAX).unwrap(),
247 };
248 assert_eq!(domain, expected);
249
250 let new_proto: pb::PositionMetadata = domain.clone().into();
251 assert_eq!(new_proto, original_proto);
252
253 let serialized = new_proto.encode_to_vec();
254 assert_eq!(serialized.len(), POSITION_METADATA_SIZE_BYTES);
255 }
256
257 #[test]
258 #[should_panic]
259 fn domain_type_invalid_zero_strategy() {
260 let proto = pb::PositionMetadata {
261 strategy: 0,
262 identifier: 1,
263 };
264 let _: PositionMetadata = proto.try_into().unwrap();
265 }
266
267 #[test]
268 #[should_panic]
269 fn domain_type_invalid_variant_invalid_id() {
270 let proto = pb::PositionMetadata {
271 strategy: 0,
272 identifier: 0,
273 };
274 let _: PositionMetadata = proto.try_into().unwrap();
275 }
276
277 #[test]
278 fn custom_strategy_lossy() {
279 let metadata = PositionMetadata {
280 strategy: NonZeroU32::new(1).unwrap(),
281 identifier: NonZeroU32::new(1).unwrap(),
282 };
283
284 let proto: pb::PositionMetadata = metadata.clone().into();
285
286 let roundtrip_metadata: PositionMetadata = proto.try_into().unwrap();
287 assert_eq!(roundtrip_metadata, metadata);
288 }
289}