darkfi/blockchain/monero/
mod.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    fmt,
21    io::{self, Cursor, Error, Read, Write},
22    iter,
23};
24
25use darkfi_sdk::{hex::decode_hex, AsHex};
26#[cfg(feature = "async-serial")]
27use darkfi_serial::{async_trait, AsyncDecodable, AsyncEncodable, AsyncRead, AsyncWrite};
28use darkfi_serial::{Decodable, Encodable};
29use monero::{
30    blockdata::transaction::{ExtraField, RawExtraField, SubField},
31    consensus::{Decodable as XmrDecodable, Encodable as XmrEncodable},
32    cryptonote::hash::Hashable,
33    util::ringct::{RctSigBase, RctType},
34    BlockHeader,
35};
36use tiny_keccak::{Hasher, Keccak};
37use tracing::warn;
38
39use crate::{Error::MoneroMergeMineError, Result};
40
41pub mod fixed_array;
42use fixed_array::{FixedByteArray, MaxSizeVec};
43
44pub mod merkle_proof;
45use merkle_proof::MerkleProof;
46
47pub mod keccak;
48use keccak::{keccak_from_bytes, keccak_to_bytes};
49
50pub mod utils;
51use utils::{create_blockhashing_blob, create_merkle_proof, tree_hash};
52
53pub mod merkle_tree_parameters;
54pub use merkle_tree_parameters::MerkleTreeParameters;
55
56pub type AuxChainHashes = MaxSizeVec<monero::Hash, 128>;
57
58/// This struct represents all the Proof of Work information required
59/// for merge mining.
60#[derive(Clone)]
61pub struct MoneroPowData {
62    /// Monero Header fields
63    pub header: BlockHeader,
64    /// RandomX VM key - length varies to a max len of 60.
65    pub randomx_key: FixedByteArray,
66    /// The number of transactions included in this Monero block.
67    /// This is used to produce the blockhashing_blob.
68    pub transaction_count: u16,
69    /// Transaction root
70    pub merkle_root: monero::Hash,
71    /// Coinbase Merkle proof hashes
72    pub coinbase_merkle_proof: MerkleProof,
73    /// Incomplete hashed state of the coinbase transaction
74    pub coinbase_tx_hasher: Keccak,
75    /// Extra field of the coinbase
76    pub coinbase_tx_extra: RawExtraField,
77    /// Aux chain Merkle proof hashes
78    pub aux_chain_merkle_proof: MerkleProof,
79}
80
81impl MoneroPowData {
82    /// Constructs the Monero PoW data from the given block and seed
83    pub fn new(
84        block: monero::Block,
85        seed: FixedByteArray,
86        aux_chain_merkle_proof: MerkleProof,
87    ) -> Result<Self> {
88        let hashes = create_ordered_tx_hashes_from_block(&block);
89        let root = tree_hash(&hashes)?;
90        let hash =
91            hashes.first().ok_or(MoneroMergeMineError("No hashes for Merkle proof".to_string()))?;
92
93        let coinbase_merkle_proof = create_merkle_proof(&hashes, hash).ok_or_else(|| {
94            MoneroMergeMineError(
95                "create_merkle_proof returned None because the block has no coinbase".to_string(),
96            )
97        })?;
98
99        let coinbase = block.miner_tx.clone();
100
101        let mut keccak = Keccak::v256();
102        let mut encoder_prefix = vec![];
103        coinbase.prefix.version.consensus_encode(&mut encoder_prefix)?;
104        coinbase.prefix.unlock_time.consensus_encode(&mut encoder_prefix)?;
105        coinbase.prefix.inputs.consensus_encode(&mut encoder_prefix)?;
106        coinbase.prefix.outputs.consensus_encode(&mut encoder_prefix)?;
107        keccak.update(&encoder_prefix);
108
109        Ok(Self {
110            header: block.header,
111            randomx_key: seed,
112            transaction_count: hashes.len() as u16,
113            merkle_root: root,
114            coinbase_merkle_proof,
115            coinbase_tx_extra: block.miner_tx.prefix.extra,
116            coinbase_tx_hasher: keccak,
117            aux_chain_merkle_proof,
118        })
119    }
120
121    /// Returns `true` if the coinbase Merkle proof produces the `merkle_root`
122    /// hash, otherwise `false`.
123    pub fn is_coinbase_valid_merkle_root(&self) -> bool {
124        let mut finalised_prefix_keccak = self.coinbase_tx_hasher.clone();
125        let mut encoder_extra_field = vec![];
126
127        self.coinbase_tx_extra.consensus_encode(&mut encoder_extra_field).unwrap();
128        finalised_prefix_keccak.update(&encoder_extra_field);
129        let mut prefix_hash: [u8; 32] = [0u8; 32];
130        finalised_prefix_keccak.finalize(&mut prefix_hash);
131
132        let final_prefix_hash = monero::Hash::from_slice(&prefix_hash);
133
134        // let mut finalised_keccak = Keccak::v256();
135        let rct_sig_base = RctSigBase {
136            rct_type: RctType::Null,
137            txn_fee: Default::default(),
138            pseudo_outs: vec![],
139            ecdh_info: vec![],
140            out_pk: vec![],
141        };
142
143        let hashes = vec![final_prefix_hash, rct_sig_base.hash(), monero::Hash::null()];
144
145        let encoder_final: Vec<u8> =
146            hashes.into_iter().flat_map(|h| Vec::from(&h.to_bytes()[..])).collect();
147
148        let coinbase_hash = monero::Hash::new(encoder_final);
149
150        let merkle_root = self.coinbase_merkle_proof.calculate_root(&coinbase_hash);
151        (self.merkle_root == merkle_root) && self.coinbase_merkle_proof.check_coinbase_path()
152    }
153
154    /// Returns the block hashing blob for the Monero block.
155    pub fn to_block_hashing_blob(&self) -> Vec<u8> {
156        create_blockhashing_blob(&self.header, &self.merkle_root, u64::from(self.transaction_count))
157    }
158
159    /// Returns the RandomX VM key
160    pub fn randomx_key(&self) -> &[u8] {
161        self.randomx_key.as_slice()
162    }
163}
164
165impl fmt::Debug for MoneroPowData {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        let mut digest = [0u8; 32];
168        self.coinbase_tx_hasher.clone().finalize(&mut digest);
169        f.debug_struct("MoneroPowData")
170            .field("header", &self.header)
171            .field("randomx_key", &self.randomx_key)
172            .field("transaction_count", &self.transaction_count)
173            .field("merkle_root", &self.merkle_root)
174            .field("coinbase_merkle_proof", &self.coinbase_merkle_proof)
175            .field("coinbase_tx_extra", &self.coinbase_tx_extra)
176            .field("aux_chain_merkle_proof", &self.aux_chain_merkle_proof)
177            .finish()
178    }
179}
180
181impl Encodable for MoneroPowData {
182    fn encode<S: Write>(&self, s: &mut S) -> io::Result<usize> {
183        let mut n = 0;
184
185        // Monero library encoding doesn't do async, so in order to
186        // match our AsyncEncodable implementation, we will write
187        // to an intermediate buffer here, as well as for any other
188        // fields that use Monero consensus encoding.
189        let mut buf = vec![];
190        self.header.consensus_encode(&mut buf)?;
191        n += buf.encode(s)?;
192
193        n += self.randomx_key.encode(s)?;
194        n += self.transaction_count.encode(s)?;
195
196        let mut buf = vec![];
197        self.merkle_root.consensus_encode(&mut buf)?;
198        n += buf.encode(s)?;
199
200        n += self.coinbase_merkle_proof.encode(s)?;
201
202        // This is an incomplete hasher. Dump it from memory
203        // and write it down. We can restore it the same way.
204        let buf = keccak_to_bytes(&self.coinbase_tx_hasher);
205        n += buf.encode(s)?;
206
207        n += self.coinbase_tx_extra.0.encode(s)?;
208        n += self.aux_chain_merkle_proof.encode(s)?;
209
210        Ok(n)
211    }
212}
213
214#[cfg(feature = "async-serial")]
215#[async_trait]
216impl AsyncEncodable for MoneroPowData {
217    async fn encode_async<S: AsyncWrite + Unpin + Send>(&self, s: &mut S) -> io::Result<usize> {
218        let mut n = 0;
219
220        // We write to an intermediate buffer since the Monero
221        // consensus encoding library doesn't do async writing.
222        let mut buf = vec![];
223        self.header.consensus_encode(&mut buf)?;
224        n += buf.encode_async(s).await?;
225
226        n += self.randomx_key.encode_async(s).await?;
227        n += self.transaction_count.encode_async(s).await?;
228
229        let mut buf = vec![];
230        self.merkle_root.consensus_encode(&mut buf)?;
231        n += buf.encode_async(s).await?;
232
233        n += self.coinbase_merkle_proof.encode_async(s).await?;
234
235        // This is an incomplete hasher. Dump it from memory
236        // and write it down. We can restore it the same way.
237        let buf = keccak_to_bytes(&self.coinbase_tx_hasher);
238        n += buf.encode_async(s).await?;
239
240        n += self.coinbase_tx_extra.0.encode_async(s).await?;
241        n += self.aux_chain_merkle_proof.encode_async(s).await?;
242
243        Ok(n)
244    }
245}
246
247impl Decodable for MoneroPowData {
248    fn decode<D: Read>(d: &mut D) -> io::Result<Self> {
249        let buf: Vec<u8> = Decodable::decode(d)?;
250        let mut buf = Cursor::new(buf);
251        let header = BlockHeader::consensus_decode(&mut buf)
252            .map_err(|_| Error::other("Invalid XMR header"))?;
253
254        let randomx_key: FixedByteArray = Decodable::decode(d)?;
255        let transaction_count: u16 = Decodable::decode(d)?;
256
257        let buf: Vec<u8> = Decodable::decode(d)?;
258        let mut buf = Cursor::new(buf);
259        let merkle_root = monero::Hash::consensus_decode(&mut buf)
260            .map_err(|_| Error::other("Invalid XMR hash"))?;
261
262        let coinbase_merkle_proof: MerkleProof = Decodable::decode(d)?;
263
264        let buf: Vec<u8> = Decodable::decode(d)?;
265        let coinbase_tx_hasher = keccak_from_bytes(&buf);
266
267        let coinbase_tx_extra: Vec<u8> = Decodable::decode(d)?;
268        let coinbase_tx_extra = RawExtraField(coinbase_tx_extra);
269        let aux_chain_merkle_proof: MerkleProof = Decodable::decode(d)?;
270
271        Ok(Self {
272            header,
273            randomx_key,
274            transaction_count,
275            merkle_root,
276            coinbase_merkle_proof,
277            coinbase_tx_hasher,
278            coinbase_tx_extra,
279            aux_chain_merkle_proof,
280        })
281    }
282}
283
284#[cfg(feature = "async-serial")]
285#[async_trait]
286impl AsyncDecodable for MoneroPowData {
287    async fn decode_async<D: AsyncRead + Unpin + Send>(d: &mut D) -> io::Result<Self> {
288        let buf: Vec<u8> = AsyncDecodable::decode_async(d).await?;
289        let mut buf = Cursor::new(buf);
290        let header = BlockHeader::consensus_decode(&mut buf)
291            .map_err(|_| Error::other("Invalid XMR header"))?;
292
293        let randomx_key: FixedByteArray = AsyncDecodable::decode_async(d).await?;
294        let transaction_count: u16 = AsyncDecodable::decode_async(d).await?;
295
296        let buf: Vec<u8> = AsyncDecodable::decode_async(d).await?;
297        let mut buf = Cursor::new(buf);
298        let merkle_root = monero::Hash::consensus_decode(&mut buf)
299            .map_err(|_| Error::other("Invalid XMR hash"))?;
300
301        let coinbase_merkle_proof: MerkleProof = AsyncDecodable::decode_async(d).await?;
302
303        let buf: Vec<u8> = AsyncDecodable::decode_async(d).await?;
304        let coinbase_tx_hasher = keccak_from_bytes(&buf);
305
306        let coinbase_tx_extra: Vec<u8> = AsyncDecodable::decode_async(d).await?;
307        let coinbase_tx_extra = RawExtraField(coinbase_tx_extra);
308        let aux_chain_merkle_proof: MerkleProof = AsyncDecodable::decode_async(d).await?;
309
310        Ok(Self {
311            header,
312            randomx_key,
313            transaction_count,
314            merkle_root,
315            coinbase_merkle_proof,
316            coinbase_tx_hasher,
317            coinbase_tx_extra,
318            aux_chain_merkle_proof,
319        })
320    }
321}
322
323/// Create a set of ordered transaction hashes from a Monero block
324pub fn create_ordered_tx_hashes_from_block(block: &monero::Block) -> Vec<monero::Hash> {
325    iter::once(block.miner_tx.hash()).chain(block.tx_hashes.clone()).collect()
326}
327
328/// Inserts aux chain merkle root and info into a Monero block
329pub fn insert_aux_chain_mr_and_info_into_block<T: AsRef<[u8]>>(
330    block: &mut monero::Block,
331    aux_chain_mr: T,
332    aux_chain_count: u8,
333    aux_nonce: u32,
334) -> Result<()> {
335    if aux_chain_count == 0 {
336        return Err(MoneroMergeMineError("Zero aux chains".to_string()))
337    }
338
339    if aux_chain_mr.as_ref().len() != monero::Hash::len_bytes() {
340        return Err(MoneroMergeMineError("Aux chain root invalid length".to_string()))
341    }
342
343    // When we insert the Merge Mining tag, we need to make sure
344    // that the extra field is valid.
345    let mut extra_field = match ExtraField::try_parse(&block.miner_tx.prefix.extra) {
346        Ok(v) => v,
347        Err(e) => return Err(MoneroMergeMineError(e.to_string())),
348    };
349
350    // Adding more than one Merge Mining tag is not allowed
351    for item in &extra_field.0 {
352        if let SubField::MergeMining(_, _) = item {
353            return Err(MoneroMergeMineError("More than one mm tag in coinbase".to_string()))
354        }
355    }
356
357    // If `SubField::Padding(n)` with `n < 255` is the last subfield in the
358    // extra field, then appending a new field will always fail to deserialize
359    // (`ExtraField::try_parse`) - the new field cannot be parsed in that
360    // sequence.
361    // To circumvent this, we create a new extra field by appending the
362    // original extra field to the merge mining field instead.
363    let hash = monero::Hash::from_slice(aux_chain_mr.as_ref());
364    let encoded = if aux_chain_count == 1 {
365        monero::VarInt(0)
366    } else {
367        let mt_params = MerkleTreeParameters::new(aux_chain_count, aux_nonce)?;
368        mt_params.to_varint()
369    };
370    extra_field.0.insert(0, SubField::MergeMining(encoded, hash));
371
372    block.miner_tx.prefix.extra = extra_field.into();
373
374    // Let's test the block to ensure it serializes correctly.
375    let blocktemplate_ser = monero::consensus::serialize(block);
376    let blocktemplate_hex = blocktemplate_ser.hex();
377    let blocktemplate_bytes = decode_hex(&blocktemplate_hex)
378        .collect::<std::result::Result<Vec<_>, _>>()
379        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
380    let de_block: monero::Block = match monero::consensus::deserialize(&blocktemplate_bytes) {
381        Ok(v) => v,
382        Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e).into()),
383    };
384
385    if block != &de_block {
386        return Err(MoneroMergeMineError("Blocks don't match after serialization".to_string()))
387    }
388
389    Ok(())
390}
391
392/*
393/// Creates a hex-encoded Monero `blockhashing_blob`
394fn create_block_hashing_blob(
395    header: &monero::BlockHeader,
396    merkle_root: &monero::Hash,
397    transaction_count: u64,
398) -> Vec<u8> {
399    let mut blockhashing_blob = monero::consensus::serialize(header);
400    blockhashing_blob.extend_from_slice(merkle_root.as_bytes());
401    let mut count = monero::consensus::serialize(&monero::VarInt(transaction_count));
402    blockhashing_blob.append(&mut count);
403    blockhashing_blob
404}
405
406/// Creates a hex-encoded Monero `blockhashing_blob` that's used by the PoW hash
407fn create_blockhashing_blob_from_blob(block: &monero::Block) -> Result<String> {
408    let tx_hashes = create_ordered_tx_hashes_from_block(block);
409    let root = tree_hash(&tx_hashes)?;
410    let blob = create_block_hashing_blob(&block.header, &root, tx_hashes.len() as u64);
411    Ok(blob.hex())
412}
413*/
414
415/// Try to decode a `monero::Block` given a hex blob
416pub fn monero_block_deserialize(blob: &str) -> Result<monero::Block> {
417    let bytes: Vec<u8> = decode_hex(blob)
418        .collect::<std::result::Result<Vec<_>, _>>()
419        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
420
421    let mut reader = Cursor::new(bytes);
422
423    match monero::Block::consensus_decode(&mut reader) {
424        Ok(v) => Ok(v),
425        Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e).into()),
426    }
427}
428
429/// Parsing an extra field from bytes will always return an extra field with
430/// subfields that could be read even if it does not represent the original
431// extra field.
432/// As per Monero consensus rules, an error here will not represent failure
433/// to deserialize a block, so no need to error here.
434fn parse_extra_field_truncate_on_error(raw_extra_field: &RawExtraField) -> ExtraField {
435    match ExtraField::try_parse(raw_extra_field) {
436        Ok(v) => v,
437        Err(v) => {
438            warn!(
439                target: "blockchain::monero::parse_extra_field_truncate_on_error",
440                "[BLOCKCHAIN] Some Monero tx_extra subfields could not be parsed",
441            );
442            v
443        }
444    }
445}
446
447/// Extract the Monero block hash from the coinbase transaction's extra field
448pub fn extract_aux_merkle_root_from_block(monero: &monero::Block) -> Result<Option<monero::Hash>> {
449    extract_aux_merkle_root(&monero.miner_tx.prefix.extra)
450}
451
452/// Extract the Monero block hash from the coinbase transaction's extra field
453pub fn extract_aux_merkle_root(extra_field: &RawExtraField) -> Result<Option<monero::Hash>> {
454    let extra_field = parse_extra_field_truncate_on_error(extra_field);
455    // Only one merge mining tag is allowed
456    let merge_mining_hashes: Vec<monero::Hash> = extra_field
457        .0
458        .iter()
459        .filter_map(|item| {
460            if let SubField::MergeMining(_depth, merge_mining_hash) = item {
461                Some(*merge_mining_hash)
462            } else {
463                None
464            }
465        })
466        .collect();
467
468    if merge_mining_hashes.len() > 1 {
469        return Err(MoneroMergeMineError("More than one MM tag found in coinbase".to_string()))
470    }
471
472    if let Some(merge_mining_hash) = merge_mining_hashes.into_iter().next() {
473        Ok(Some(merge_mining_hash))
474    } else {
475        Ok(None)
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use std::str::FromStr;
483
484    // Blob from Monero testnet, height 2912484, mergemined DarkFi.
485    const XMR_BLOCK: &str = "1010f881efca0644a1185eeccb2629b316ec0d41659111299ad1b736a3b0d8eac8bbc6384dc5c84bb6010002a0e2b10101ffe4e1b1010180e0a596bb1103f1d23951bd28ce2bfad791f2350e2ac348e4620e19af3418653a1839cc5c8f2be14a010b204d874ed5087b649c711dd4479434a85dbf7e9bdfae26f5bc785964d4b45c0204751b43e10321082d5f403be836d45d026fbaa2a8e4b4a9d0d821f29d709321f8d764f32d446fa80000";
486    const SEED_HASH: &str = "f1d23951bd28ce2bfad791f2350e2ac348e4620e19af3418653a1839cc5c8f2b";
487
488    // Test that both sync and async serialization formats match.
489    // We do some hacks because Monero lib doesn't do async.
490    #[test]
491    fn test_monero_powdata_serde() {
492        let block = monero_block_deserialize(XMR_BLOCK).unwrap();
493        let seed = FixedByteArray::from_bytes(&hex::decode(SEED_HASH).unwrap()).unwrap();
494
495        // The Merkle proof is fake to keep it simple.
496        let tx_hashes = &[
497            "d96756959949db23764592fea0bfe88c790e1fd131dabb676948b343aa9ecc24",
498            "77d1a87df131c36da4832a7ec382db9b8fe947576a60ec82cc1c66a220f6ee42",
499        ]
500        .iter()
501        .map(|hash| monero::Hash::from_str(hash).unwrap())
502        .collect::<Vec<_>>();
503
504        let aux_chain_merkle_proof = create_merkle_proof(tx_hashes, &tx_hashes[0]).unwrap();
505
506        // Construct PowData
507        let mut powdata = MoneroPowData::new(block, seed, aux_chain_merkle_proof).unwrap();
508
509        let local_ex = smol::LocalExecutor::new();
510
511        let ser_sync = darkfi_serial::serialize(&powdata);
512        let ser_async = smol::future::block_on(
513            local_ex.run(async { darkfi_serial::serialize_async(&powdata).await }),
514        );
515
516        assert_eq!(ser_sync, ser_async);
517
518        let mut de_sync: MoneroPowData = darkfi_serial::deserialize(&ser_async).unwrap();
519        let mut de_async: MoneroPowData = smol::future::block_on(
520            local_ex.run(async { darkfi_serial::deserialize_async(&ser_async).await.unwrap() }),
521        );
522
523        assert_eq!(de_sync.header, powdata.header);
524        assert_eq!(de_sync.randomx_key, powdata.randomx_key);
525        assert_eq!(de_sync.transaction_count, powdata.transaction_count);
526        assert_eq!(de_sync.merkle_root, powdata.merkle_root);
527        assert_eq!(de_sync.coinbase_merkle_proof.branch(), powdata.coinbase_merkle_proof.branch());
528        assert_eq!(de_sync.coinbase_merkle_proof.path(), powdata.coinbase_merkle_proof.path());
529        assert_eq!(de_sync.coinbase_tx_extra, powdata.coinbase_tx_extra);
530        assert_eq!(
531            de_sync.aux_chain_merkle_proof.branch(),
532            powdata.aux_chain_merkle_proof.branch()
533        );
534        assert_eq!(de_sync.aux_chain_merkle_proof.path(), powdata.aux_chain_merkle_proof.path());
535
536        assert_eq!(de_async.header, powdata.header);
537        assert_eq!(de_async.randomx_key, powdata.randomx_key);
538        assert_eq!(de_async.transaction_count, powdata.transaction_count);
539        assert_eq!(de_async.merkle_root, powdata.merkle_root);
540        assert_eq!(de_async.coinbase_merkle_proof.branch(), powdata.coinbase_merkle_proof.branch());
541        assert_eq!(de_async.coinbase_merkle_proof.path(), powdata.coinbase_merkle_proof.path());
542        assert_eq!(de_async.coinbase_tx_extra, powdata.coinbase_tx_extra);
543        assert_eq!(
544            de_async.aux_chain_merkle_proof.branch(),
545            powdata.aux_chain_merkle_proof.branch()
546        );
547        assert_eq!(de_async.aux_chain_merkle_proof.path(), powdata.aux_chain_merkle_proof.path());
548
549        // Keccak state
550        powdata.coinbase_tx_hasher.update(b"hi");
551        let mut powdata_digest = vec![];
552        powdata.coinbase_tx_hasher.finalize(&mut powdata_digest);
553
554        de_sync.coinbase_tx_hasher.update(b"hi");
555        let mut de_sync_digest = vec![];
556        de_sync.coinbase_tx_hasher.finalize(&mut de_sync_digest);
557
558        de_async.coinbase_tx_hasher.update(b"hi");
559        let mut de_async_digest = vec![];
560        de_async.coinbase_tx_hasher.finalize(&mut de_async_digest);
561
562        assert_eq!(de_sync_digest, powdata_digest);
563        assert_eq!(de_async_digest, powdata_digest);
564    }
565}