1use std::fmt::Display;
7use std::ops::Rem;
8use std::str::FromStr;
9
10use anyhow::Context;
11use fake::Dummy;
12use pathfinder_crypto::hash::HashChain;
13use pathfinder_crypto::Felt;
14use primitive_types::H160;
15use serde::{Deserialize, Serialize};
16
17pub mod casm_class;
18pub mod class_definition;
19pub mod consensus_info;
20pub mod consts;
21pub mod event;
22pub mod hash;
23mod header;
24pub mod integration_testing;
25mod l1;
26mod l2;
27mod macros;
28pub mod prelude;
29pub mod receipt;
30pub mod signature;
31pub mod state_update;
32pub mod test_utils;
33pub mod transaction;
34pub mod trie;
35
36pub use header::{BlockHeader, BlockHeaderBuilder, L1DataAvailabilityMode, SignedBlockHeader};
37pub use l1::{L1BlockHash, L1BlockNumber, L1TransactionHash};
38pub use l2::{
39 ConsensusFinalizedBlockHeader,
40 ConsensusFinalizedL2Block,
41 DeclaredClass,
42 L2Block,
43 L2BlockToCommit,
44};
45pub use signature::BlockCommitmentSignature;
46pub use state_update::{FoundStorageValue, StateUpdate};
47
48impl ContractAddress {
49 pub const ONE: ContractAddress = contract_address!("0x1");
55 pub const TWO: ContractAddress = contract_address!("0x2");
64 pub const SYSTEM: [ContractAddress; 2] = [ContractAddress::ONE, ContractAddress::TWO];
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct ContractClass {
71 pub program: String,
73 pub entry_points_by_type: serde_json::Value,
78}
79
80impl EntryPoint {
81 pub fn hashed(input: &[u8]) -> Self {
86 use sha3::Digest;
87 EntryPoint(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
88 input,
89 ))))
90 }
91
92 pub const CONSTRUCTOR: Self =
95 entry_point!("0x028FFE4FF0F226A9107253E17A904099AA4F63A02A5621DE0576E5AA71BC5194");
96}
97
98impl StateCommitment {
99 pub fn calculate(
111 storage_commitment: StorageCommitment,
112 class_commitment: ClassCommitment,
113 version: StarknetVersion,
114 ) -> Self {
115 if class_commitment == ClassCommitment::ZERO
116 && storage_commitment == StorageCommitment::ZERO
117 {
118 return StateCommitment::ZERO;
119 }
120
121 if class_commitment == ClassCommitment::ZERO && version < StarknetVersion::V_0_14_0 {
122 return Self(storage_commitment.0);
123 }
124
125 const GLOBAL_STATE_VERSION: Felt = felt_bytes!(b"STARKNET_STATE_V0");
126
127 StateCommitment(
128 pathfinder_crypto::hash::poseidon::poseidon_hash_many(&[
129 GLOBAL_STATE_VERSION.into(),
130 storage_commitment.0.into(),
131 class_commitment.0.into(),
132 ])
133 .into(),
134 )
135 }
136}
137
138impl StorageAddress {
139 pub fn from_name(input: &[u8]) -> Self {
140 use sha3::Digest;
141 Self(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
142 input,
143 ))))
144 }
145
146 pub fn from_map_name_and_key(name: &[u8], key: Felt) -> Self {
147 use sha3::Digest;
148
149 let intermediate = truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(name)));
150 let value = pathfinder_crypto::hash::pedersen_hash(intermediate, key);
151
152 let value = primitive_types::U256::from_big_endian(value.as_be_bytes());
153 let max_address = primitive_types::U256::from_str_radix(
154 "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
155 16,
156 )
157 .unwrap();
158
159 let value = value.rem(max_address);
160 let mut b = [0u8; 32];
161 value.to_big_endian(&mut b);
162 Self(Felt::from_be_slice(&b).expect("Truncated value should fit into a felt"))
163 }
164}
165
166#[derive(Copy, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub struct BlockNumber(u64);
169
170macros::i64_backed_u64::new_get_partialeq!(BlockNumber);
171macros::i64_backed_u64::serdes!(BlockNumber);
172
173impl From<BlockNumber> for Felt {
174 fn from(x: BlockNumber) -> Self {
175 Felt::from(x.0)
176 }
177}
178
179impl std::iter::Iterator for BlockNumber {
180 type Item = BlockNumber;
181
182 fn next(&mut self) -> Option<Self::Item> {
183 Some(*self + 1)
184 }
185}
186
187#[derive(Copy, Debug, Clone, PartialEq, Eq, Default)]
189pub struct BlockTimestamp(u64);
190
191macros::i64_backed_u64::new_get_partialeq!(BlockTimestamp);
192macros::i64_backed_u64::serdes!(BlockTimestamp);
193
194#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
196pub struct TransactionIndex(u64);
197
198macros::i64_backed_u64::new_get_partialeq!(TransactionIndex);
199macros::i64_backed_u64::serdes!(TransactionIndex);
200
201#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
203pub struct GasPrice(pub u128);
204
205#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
207pub struct GasPriceHex(pub GasPrice);
208
209#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
211pub struct ResourceAmount(pub u64);
212
213#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
216pub struct Tip(pub u64);
217
218#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Dummy)]
220pub struct TipHex(pub Tip);
221
222#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
224pub struct ResourcePricePerUnit(pub u128);
225
226#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
228pub struct TransactionVersion(pub Felt);
229
230impl TransactionVersion {
231 pub fn is_zero(&self) -> bool {
233 self.without_query_version() == 0
234 }
235
236 pub fn without_query_version(&self) -> u128 {
242 let lower = &self.0.as_be_bytes()[16..];
243 u128::from_be_bytes(lower.try_into().expect("slice should be the right length"))
244 }
245
246 pub const fn with_query_version(self) -> Self {
247 let mut bytes = self.0.to_be_bytes();
248 bytes[15] |= 0b0000_0001;
249
250 let felt = match Felt::from_be_bytes(bytes) {
251 Ok(x) => x,
252 Err(_) => panic!("Adding query bit to transaction version failed."),
253 };
254 Self(felt)
255 }
256
257 pub const fn has_query_version(&self) -> bool {
258 self.0.as_be_bytes()[15] & 0b0000_0001 != 0
259 }
260
261 pub fn with_query_only(self, query_only: bool) -> Self {
262 if query_only {
263 self.with_query_version()
264 } else {
265 Self(self.without_query_version().into())
266 }
267 }
268
269 pub const ZERO: Self = Self(Felt::ZERO);
270 pub const ONE: Self = Self(Felt::from_u64(1));
271 pub const TWO: Self = Self(Felt::from_u64(2));
272 pub const THREE: Self = Self(Felt::from_u64(3));
273 pub const ZERO_WITH_QUERY_VERSION: Self = Self::ZERO.with_query_version();
274 pub const ONE_WITH_QUERY_VERSION: Self = Self::ONE.with_query_version();
275 pub const TWO_WITH_QUERY_VERSION: Self = Self::TWO.with_query_version();
276 pub const THREE_WITH_QUERY_VERSION: Self = Self::THREE.with_query_version();
277}
278
279#[derive(Debug, Copy, Clone, PartialEq, Eq)]
283pub enum BlockId {
284 Number(BlockNumber),
285 Hash(BlockHash),
286 Latest,
287}
288
289impl BlockId {
290 pub fn is_latest(&self) -> bool {
291 self == &Self::Latest
292 }
293}
294
295impl BlockNumber {
296 pub const GENESIS: BlockNumber = BlockNumber::new_or_panic(0);
297 pub const MAX: BlockNumber = BlockNumber::new_or_panic(i64::MAX as u64);
300
301 pub fn parent(&self) -> Option<Self> {
304 if self == &Self::GENESIS {
305 None
306 } else {
307 Some(*self - 1)
308 }
309 }
310
311 pub fn is_zero(&self) -> bool {
312 self == &Self::GENESIS
313 }
314
315 pub fn checked_add(&self, rhs: u64) -> Option<Self> {
316 Self::new(self.0.checked_add(rhs)?)
317 }
318
319 pub fn checked_sub(&self, rhs: u64) -> Option<Self> {
320 self.0.checked_sub(rhs).map(Self)
321 }
322
323 pub fn saturating_sub(&self, rhs: u64) -> Self {
324 Self(self.0.saturating_sub(rhs))
325 }
326}
327
328impl std::ops::Add<u64> for BlockNumber {
329 type Output = BlockNumber;
330
331 fn add(self, rhs: u64) -> Self::Output {
332 Self(self.0 + rhs)
333 }
334}
335
336impl std::ops::AddAssign<u64> for BlockNumber {
337 fn add_assign(&mut self, rhs: u64) {
338 self.0 += rhs;
339 }
340}
341
342impl std::ops::Sub<u64> for BlockNumber {
343 type Output = BlockNumber;
344
345 fn sub(self, rhs: u64) -> Self::Output {
346 Self(self.0 - rhs)
347 }
348}
349
350impl std::ops::SubAssign<u64> for BlockNumber {
351 fn sub_assign(&mut self, rhs: u64) {
352 self.0 -= rhs;
353 }
354}
355
356#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
358pub struct EthereumAddress(pub H160);
359
360impl<T> Dummy<T> for EthereumAddress {
361 fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &T, rng: &mut R) -> Self {
362 Self(H160::random_using(rng))
363 }
364}
365
366#[derive(Debug, thiserror::Error)]
367#[error("expected slice length of 16 or less, got {0}")]
368pub struct FromSliceError(usize);
369
370impl GasPrice {
371 pub const ZERO: GasPrice = GasPrice(0u128);
372
373 pub fn to_be_bytes(&self) -> [u8; 16] {
375 self.0.to_be_bytes()
376 }
377
378 pub fn from_be_bytes(src: [u8; 16]) -> Self {
381 Self(u128::from_be_bytes(src))
382 }
383
384 pub fn from_be_slice(src: &[u8]) -> Result<Self, FromSliceError> {
387 if src.len() > 16 {
388 return Err(FromSliceError(src.len()));
389 }
390
391 let mut buf = [0u8; 16];
392 buf[16 - src.len()..].copy_from_slice(src);
393
394 Ok(Self::from_be_bytes(buf))
395 }
396}
397
398impl From<u64> for GasPrice {
399 fn from(src: u64) -> Self {
400 Self(u128::from(src))
401 }
402}
403
404impl TryFrom<Felt> for GasPrice {
405 type Error = anyhow::Error;
406
407 fn try_from(src: Felt) -> Result<Self, Self::Error> {
408 anyhow::ensure!(
409 src.as_be_bytes()[0..16] == [0; 16],
410 "Gas price fits into u128"
411 );
412
413 let mut bytes = [0u8; 16];
414 bytes.copy_from_slice(&src.as_be_bytes()[16..]);
415 Ok(Self(u128::from_be_bytes(bytes)))
416 }
417}
418
419impl From<BlockNumber> for BlockId {
420 fn from(number: BlockNumber) -> Self {
421 Self::Number(number)
422 }
423}
424
425impl From<BlockHash> for BlockId {
426 fn from(hash: BlockHash) -> Self {
427 Self::Hash(hash)
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum EthereumChain {
434 Mainnet,
435 Sepolia,
436 Other(primitive_types::U256),
437}
438
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub enum Chain {
442 Mainnet,
443 SepoliaTestnet,
444 SepoliaIntegration,
445 Custom,
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
449pub struct ChainId(pub Felt);
450
451impl ChainId {
452 const fn from_slice_unwrap(slice: &[u8]) -> Self {
454 Self(match Felt::from_be_slice(slice) {
455 Ok(v) => v,
456 Err(_) => panic!("Bad value"),
457 })
458 }
459
460 pub fn to_hex_str(&self) -> std::borrow::Cow<'static, str> {
463 self.0.to_hex_str()
464 }
465
466 pub fn as_str(&self) -> &str {
468 std::str::from_utf8(self.0.as_be_bytes())
469 .expect("valid utf8")
470 .trim_start_matches('\0')
471 }
472
473 pub const MAINNET: Self = Self::from_slice_unwrap(b"SN_MAIN");
474 pub const SEPOLIA_TESTNET: Self = Self::from_slice_unwrap(b"SN_SEPOLIA");
475 pub const SEPOLIA_INTEGRATION: Self = Self::from_slice_unwrap(b"SN_INTEGRATION_SEPOLIA");
476}
477
478impl std::fmt::Display for Chain {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 match self {
481 Chain::Mainnet => f.write_str("Mainnet"),
482 Chain::SepoliaTestnet => f.write_str("Testnet/Sepolia"),
483 Chain::SepoliaIntegration => f.write_str("Integration/Sepolia"),
484 Chain::Custom => f.write_str("Custom"),
485 }
486 }
487}
488
489#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Dummy)]
490pub struct StarknetVersion(u8, u8, u8, u8);
491
492impl StarknetVersion {
493 pub const fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
494 StarknetVersion(a, b, c, d)
495 }
496
497 pub fn as_u32(&self) -> u32 {
498 u32::from_le_bytes([self.0, self.1, self.2, self.3])
499 }
500
501 pub fn from_u32(version: u32) -> Self {
502 let [a, b, c, d] = version.to_le_bytes();
503 StarknetVersion(a, b, c, d)
504 }
505
506 pub const V_0_13_2: Self = Self::new(0, 13, 2, 0);
507
508 pub const V_0_13_4: Self = Self::new(0, 13, 4, 0);
511 pub const V_0_14_0: Self = Self::new(0, 14, 0, 0);
514}
515
516impl FromStr for StarknetVersion {
517 type Err = anyhow::Error;
518
519 fn from_str(s: &str) -> Result<Self, Self::Err> {
520 if s.is_empty() {
521 return Ok(StarknetVersion::new(0, 0, 0, 0));
522 }
523
524 let parts: Vec<_> = s.split('.').collect();
525 anyhow::ensure!(
526 parts.len() == 3 || parts.len() == 4,
527 "Invalid version string, expected 3 or 4 parts but got {}",
528 parts.len()
529 );
530
531 let a = parts[0].parse()?;
532 let b = parts[1].parse()?;
533 let c = parts[2].parse()?;
534 let d = parts.get(3).map(|x| x.parse()).transpose()?.unwrap_or(0);
535
536 Ok(StarknetVersion(a, b, c, d))
537 }
538}
539
540impl Display for StarknetVersion {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 if self.0 == 0 && self.1 == 0 && self.2 == 0 && self.3 == 0 {
543 return Ok(());
544 }
545 if self.3 == 0 {
546 write!(f, "{}.{}.{}", self.0, self.1, self.2)
547 } else {
548 write!(f, "{}.{}.{}.{}", self.0, self.1, self.2, self.3)
549 }
550 }
551}
552
553macros::felt_newtypes!(
554 [
555 AccountDeploymentDataElem,
556 BlockHash,
557 ByteCodeOffset,
558 BlockCommitmentSignatureElem,
559 CallParam,
560 CallResultValue,
561 ClassCommitment,
562 ClassCommitmentLeafHash,
563 ConstructorParam,
564 ContractAddressSalt,
565 ContractNonce,
566 ContractStateHash,
567 ContractRoot,
568 EntryPoint,
569 EventCommitment,
570 EventData,
571 EventKey,
572 Fee,
573 L1ToL2MessageNonce,
574 L1ToL2MessagePayloadElem,
575 L2ToL1MessagePayloadElem,
576 PaymasterDataElem,
577 ProofFactElem,
578 ProposalCommitment,
579 PublicKey,
580 SequencerAddress,
581 StateCommitment,
582 StateDiffCommitment,
583 StorageCommitment,
584 StorageValue,
585 TransactionCommitment,
586 ReceiptCommitment,
587 TransactionHash,
588 TransactionNonce,
589 TransactionSignatureElem,
590 ];
591 [
592 CasmHash,
593 ClassHash,
594 ContractAddress,
595 SierraHash,
596 StorageAddress,
597 ]
598);
599
600macros::fmt::thin_display!(BlockNumber);
601macros::fmt::thin_display!(BlockTimestamp);
602
603impl ContractAddress {
604 pub fn deployed_contract_address(
605 constructor_calldata: impl Iterator<Item = CallParam>,
606 contract_address_salt: &ContractAddressSalt,
607 class_hash: &ClassHash,
608 ) -> Self {
609 let constructor_calldata_hash = constructor_calldata
610 .fold(HashChain::default(), |mut h, param| {
611 h.update(param.0);
612 h
613 })
614 .finalize();
615
616 let contract_address = [
617 Felt::from_be_slice(b"STARKNET_CONTRACT_ADDRESS").expect("prefix is convertible"),
618 Felt::ZERO,
619 contract_address_salt.0,
620 class_hash.0,
621 constructor_calldata_hash,
622 ]
623 .into_iter()
624 .fold(HashChain::default(), |mut h, e| {
625 h.update(e);
626 h
627 })
628 .finalize();
629
630 const MAX_CONTRACT_ADDRESS: Felt =
632 felt!("0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00");
633 let contract_address = if contract_address >= MAX_CONTRACT_ADDRESS {
634 contract_address - MAX_CONTRACT_ADDRESS
635 } else {
636 contract_address
637 };
638
639 ContractAddress::new_or_panic(contract_address)
640 }
641
642 pub fn is_system_contract(&self) -> bool {
643 (*self == ContractAddress::ONE) || (*self == ContractAddress::TWO)
644 }
645}
646
647impl From<ContractAddress> for Vec<u8> {
648 fn from(value: ContractAddress) -> Self {
649 value.0.to_be_bytes().to_vec()
650 }
651}
652
653#[derive(Clone, Debug, PartialEq)]
654pub enum AllowedOrigins {
655 Any,
656 List(Vec<String>),
657}
658
659impl<S> From<S> for AllowedOrigins
660where
661 S: ToString,
662{
663 fn from(value: S) -> Self {
664 let s = value.to_string();
665
666 if s == "*" {
667 Self::Any
668 } else {
669 Self::List(vec![s])
670 }
671 }
672}
673
674pub fn truncated_keccak(mut plain: [u8; 32]) -> Felt {
677 plain[0] &= 0x03;
680 Felt::from_be_bytes(plain).expect("cannot overflow: smaller than modulus")
681}
682
683pub fn calculate_class_commitment_leaf_hash(
687 compiled_class_hash: CasmHash,
688) -> ClassCommitmentLeafHash {
689 const CONTRACT_CLASS_HASH_VERSION: pathfinder_crypto::Felt =
690 felt_bytes!(b"CONTRACT_CLASS_LEAF_V0");
691 ClassCommitmentLeafHash(
692 pathfinder_crypto::hash::poseidon_hash(
693 CONTRACT_CLASS_HASH_VERSION.into(),
694 compiled_class_hash.0.into(),
695 )
696 .into(),
697 )
698}
699
700#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
702pub struct Proof(pub Vec<u8>);
703
704impl Proof {
705 pub fn is_empty(&self) -> bool {
706 self.0.is_empty()
707 }
708}
709
710impl serde::Serialize for Proof {
711 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
712 use base64::Engine;
713
714 let encoded = base64::engine::general_purpose::STANDARD.encode(&self.0);
715 serializer.serialize_str(&encoded)
716 }
717}
718
719impl<'de> serde::Deserialize<'de> for Proof {
720 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
721 use base64::Engine;
722
723 let s = String::deserialize(deserializer)?;
724 if s.is_empty() {
725 return Ok(Proof::default());
726 }
727 let bytes = base64::engine::general_purpose::STANDARD
728 .decode(&s)
729 .map_err(serde::de::Error::custom)?;
730 Ok(Proof(bytes))
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use crate::{felt, CallParam, ClassHash, ContractAddress, ContractAddressSalt};
737
738 #[test]
739 fn constructor_entry_point() {
740 use sha3::{Digest, Keccak256};
741
742 use crate::{truncated_keccak, EntryPoint};
743
744 let mut keccak = Keccak256::default();
745 keccak.update(b"constructor");
746 let expected = EntryPoint(truncated_keccak(<[u8; 32]>::from(keccak.finalize())));
747
748 assert_eq!(EntryPoint::CONSTRUCTOR, expected);
749 }
750
751 mod starknet_version {
752 use std::str::FromStr;
753
754 use super::super::StarknetVersion;
755
756 #[test]
757 fn valid_version_parsing() {
758 let cases = [
759 ("1.2.3.4", "1.2.3.4", StarknetVersion::new(1, 2, 3, 4)),
760 ("1.2.3", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
761 ("1.2.3.0", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
762 ("", "", StarknetVersion::new(0, 0, 0, 0)),
763 ];
764
765 for (input, output, actual) in cases.iter() {
766 let version = StarknetVersion::from_str(input).unwrap();
767 assert_eq!(version, *actual);
768 assert_eq!(version.to_string(), *output);
769 }
770 }
771
772 #[test]
773 fn invalid_version_parsing() {
774 assert!(StarknetVersion::from_str("1.2").is_err());
775 assert!(StarknetVersion::from_str("1").is_err());
776 assert!(StarknetVersion::from_str("1.2.a").is_err());
777 }
778 }
779
780 #[test]
781 fn deployed_contract_address() {
782 let expected_contract_address = ContractAddress(felt!(
783 "0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50"
784 ));
785 let actual_contract_address = ContractAddress::deployed_contract_address(
786 std::iter::once(CallParam(felt!(
787 "0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c"
788 ))),
789 &ContractAddressSalt(felt!("0x0")),
790 &ClassHash(felt!(
791 "0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738"
792 )),
793 );
794 assert_eq!(actual_contract_address, expected_contract_address);
795 }
796
797 mod proof_serde {
798 use super::super::Proof;
799
800 #[test]
801 fn round_trip() {
802 let proof = Proof(vec![0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 1, 200]);
803 let json = serde_json::to_string(&proof).unwrap();
804 assert_eq!(json, r#""AAAAAAAAAHsAAAHI""#);
805 let deserialized: Proof = serde_json::from_str(&json).unwrap();
806 assert_eq!(deserialized, proof);
807 }
808
809 #[test]
810 fn empty_string_deserializes_to_default() {
811 let proof: Proof = serde_json::from_str(r#""""#).unwrap();
812 assert_eq!(proof, Proof::default());
813 }
814
815 #[test]
816 fn invalid_base64_returns_error() {
817 let result = serde_json::from_str::<Proof>(r#""not-valid-base64!@#""#);
818 assert!(result.is_err());
819 }
820
821 #[test]
822 fn empty_proof_serializes_to_empty_string() {
823 let proof = Proof::default();
824 let json = serde_json::to_string(&proof).unwrap();
825 assert_eq!(json, r#""""#);
826 }
827 }
828}