Skip to main content

chia_sdk_driver/offers/
compress.rs

1use std::{io::Read, sync::LazyLock};
2
3use chia_protocol::SpendBundle;
4use chia_puzzles::{
5    CAT_PUZZLE, NFT_METADATA_UPDATER_DEFAULT, NFT_OWNERSHIP_LAYER,
6    NFT_OWNERSHIP_TRANSFER_PROGRAM_ONE_WAY_CLAIM_WITH_ROYALTIES, NFT_STATE_LAYER,
7    P2_DELEGATED_PUZZLE_OR_HIDDEN_PUZZLE, SETTLEMENT_PAYMENT, SINGLETON_TOP_LAYER_V1_1,
8};
9use chia_sdk_utils::Bech32;
10use chia_traits::Streamable;
11use flate2::{
12    Compress, Compression, Decompress, FlushDecompress,
13    read::{ZlibDecoder, ZlibEncoder},
14};
15use hex_literal::hex;
16
17use crate::DriverError;
18
19pub fn compress_offer(spend_bundle: &SpendBundle) -> Result<Vec<u8>, DriverError> {
20    compress_offer_bytes(&spend_bundle.to_bytes()?)
21}
22
23pub fn decompress_offer(bytes: &[u8]) -> Result<SpendBundle, DriverError> {
24    Ok(SpendBundle::from_bytes(&decompress_offer_bytes(bytes)?)?)
25}
26
27pub fn encode_offer(spend_bundle: &SpendBundle) -> Result<String, DriverError> {
28    Ok(Bech32::new(compress_offer(spend_bundle)?.into(), "offer".to_string()).encode()?)
29}
30
31pub fn decode_offer(text: &str) -> Result<SpendBundle, DriverError> {
32    decompress_offer(&Bech32::decode(text)?.expect_prefix("offer")?)
33}
34
35const CAT_PUZZLE_V1: [u8; 1420] = hex!(
36    "
37    ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff05
38    80ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff
39    02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057f
40    ff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ff
41    ffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81
42    ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff
43    02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0b
44    ff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff80
45    80808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ff
46    ff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff
47    01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff
48    2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff
49    82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01
50    ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff
51    088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780
52    ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff
53    11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02
54    ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ff
55    ff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff
56    04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0b
57    ff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff
58    09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff0109
59    80ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080
60    ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff
61    808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff1780
62    8080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ff
63    ff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff
64    2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff
65    02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff
66    0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff
67    02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff
68    0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04
69    ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff
70    04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05
71    ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ff
72    ff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ff
73    ff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ff
74    ffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04
75    ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff
76    02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff
77    04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05
78    ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f
79    80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10
80    ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff8080
81    8080808080808080ff018080
82    "
83);
84
85const SETTLEMENT_PAYMENT_V1: [u8; 267] = hex!(
86    "
87    ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff
88    333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff
89    02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff
90    04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff
91    0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff
92    02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ff
93    ff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff8080
94    8080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101
95    ff058080ff0180ff018080
96    "
97);
98
99static COMPRESSION_ZDICT: LazyLock<Vec<u8>> = LazyLock::new(|| {
100    let mut bytes = Vec::new();
101    bytes.extend_from_slice(&P2_DELEGATED_PUZZLE_OR_HIDDEN_PUZZLE);
102    bytes.extend_from_slice(&CAT_PUZZLE_V1);
103    bytes.extend_from_slice(&SETTLEMENT_PAYMENT_V1);
104    bytes.extend_from_slice(&SINGLETON_TOP_LAYER_V1_1);
105    bytes.extend_from_slice(&NFT_STATE_LAYER);
106    bytes.extend_from_slice(&NFT_OWNERSHIP_LAYER);
107    bytes.extend_from_slice(&NFT_METADATA_UPDATER_DEFAULT);
108    bytes.extend_from_slice(&NFT_OWNERSHIP_TRANSFER_PROGRAM_ONE_WAY_CLAIM_WITH_ROYALTIES);
109    bytes.extend_from_slice(&CAT_PUZZLE);
110    bytes.extend_from_slice(&SETTLEMENT_PAYMENT);
111    bytes
112});
113
114pub fn compress_offer_bytes(bytes: &[u8]) -> Result<Vec<u8>, DriverError> {
115    let mut output = 6u16.to_be_bytes().to_vec();
116    output.extend(zlib_compress(bytes, &COMPRESSION_ZDICT)?);
117    Ok(output)
118}
119
120pub fn decompress_offer_bytes(bytes: &[u8]) -> Result<Vec<u8>, DriverError> {
121    let version_bytes: [u8; 2] = bytes
122        .get(0..2)
123        .ok_or(DriverError::MissingVersionPrefix)?
124        .try_into()?;
125
126    let version = u16::from_be_bytes(version_bytes);
127
128    if version > 6 {
129        return Err(DriverError::UnsupportedVersion);
130    }
131
132    zlib_decompress(&bytes[2..], &COMPRESSION_ZDICT)
133}
134
135pub fn zlib_compress(input: &[u8], zdict: &[u8]) -> std::io::Result<Vec<u8>> {
136    let mut compress = Compress::new(Compression::new(6), true);
137    compress.set_dictionary(zdict)?;
138    let mut encoder = ZlibEncoder::new_with_compress(input, compress);
139    let mut output = Vec::new();
140    encoder.read_to_end(&mut output)?;
141    Ok(output)
142}
143
144// 6 MB matches the limit in chia-blockchain
145// see https://github.com/Chia-Network/chia-blockchain/blob/383e1d4897738719ad945300f9be2dffbfa54f58/chia/wallet/util/puzzle_compression.py#L70
146const MAX_DECOMPRESSED_SIZE: usize = 6 * 1024 * 1024; // 6 MB
147
148pub fn zlib_decompress(input: &[u8], zdict: &[u8]) -> Result<Vec<u8>, DriverError> {
149    let mut decompress = Decompress::new(true);
150
151    if decompress
152        .decompress(input, &mut [], FlushDecompress::Finish)
153        .is_ok()
154    {
155        return Err(DriverError::NotCompressed);
156    }
157
158    decompress.set_dictionary(zdict)?;
159    let i = decompress.total_in();
160    let mut decoder = ZlibDecoder::new_with_decompress(&input[usize::try_from(i)?..], decompress);
161    let mut output = Vec::new();
162    decoder
163        .by_ref()
164        .take(MAX_DECOMPRESSED_SIZE as u64)
165        .read_to_end(&mut output)?;
166
167    if decoder.read(&mut [0u8])? > 0 {
168        return Err(DriverError::DecompressionTooLarge);
169    }
170
171    Ok(output)
172}
173
174#[cfg(test)]
175mod tests {
176    use chia_protocol::SpendBundle;
177    use chia_traits::Streamable;
178
179    use super::*;
180
181    const COMPRESSED_OFFER: &str = include_str!("./test_data/compressed.offer");
182    const DECOMPRESSED_OFFER: &str = include_str!("./test_data/decompressed.offer");
183
184    #[test]
185    fn test_compression() {
186        let decompressed_offer = hex::decode(DECOMPRESSED_OFFER.trim()).unwrap();
187        let output = compress_offer_bytes(&decompressed_offer).unwrap();
188        assert_eq!(hex::encode(output), COMPRESSED_OFFER.trim());
189    }
190
191    #[test]
192    fn test_decompression() {
193        let compressed_offer = hex::decode(COMPRESSED_OFFER.trim()).unwrap();
194        let output = decompress_offer_bytes(&compressed_offer).unwrap();
195        assert_eq!(hex::encode(output), DECOMPRESSED_OFFER.trim());
196    }
197
198    #[test]
199    fn parse_spend_bundle() {
200        let decompressed_offer = hex::decode(DECOMPRESSED_OFFER.trim()).unwrap();
201        SpendBundle::from_bytes(&decompressed_offer).unwrap();
202    }
203
204    #[test]
205    fn test_decompression_too_large() {
206        let zdict = &COMPRESSION_ZDICT;
207        let data = vec![0u8; MAX_DECOMPRESSED_SIZE + 1];
208        let compressed = zlib_compress(&data, zdict).unwrap();
209        let result = zlib_decompress(&compressed, zdict);
210        assert!(matches!(result, Err(DriverError::DecompressionTooLarge)));
211    }
212
213    #[test]
214    fn test_decompression_at_limit() {
215        let zdict = &COMPRESSION_ZDICT;
216        let data = vec![0u8; MAX_DECOMPRESSED_SIZE];
217        let compressed = zlib_compress(&data, zdict).unwrap();
218        let output = zlib_decompress(&compressed, zdict).unwrap();
219        assert_eq!(output.len(), MAX_DECOMPRESSED_SIZE);
220    }
221
222    #[test]
223    fn test_encode_decode_offer_data() {
224        let offer = b"hello world";
225        let encoded = Bech32::new(offer.to_vec().into(), "offer".to_string())
226            .encode()
227            .unwrap();
228        let decoded = Bech32::decode(&encoded)
229            .unwrap()
230            .expect_prefix("offer")
231            .unwrap();
232        assert_eq!(offer, decoded.as_slice());
233    }
234}