chain: move sha256d to serialization module.
This extracts the SHA256d code from being split across two modules and puts it in one module, under serialization. The code is unchanged except for three deleted tests: * `sha256d_flush` in `sha256d_writer` (not a meaningful test); * `transactionhash_debug` (constructs an invalid transaction hash, and the behavior is tested in the next test); * `decode_state_debug` (we do not need to test the Debug output of DecodeState);
This commit is contained in:
parent
9f31e551c9
commit
b36fe8f937
|
@ -4,9 +4,8 @@ use std::{fmt, io};
|
||||||
use proptest_derive::Arbitrary;
|
use proptest_derive::Arbitrary;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::serialization::{
|
||||||
serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize},
|
sha256d, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize,
|
||||||
sha256d_writer::Sha256dWriter,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::BlockHeader;
|
use super::BlockHeader;
|
||||||
|
@ -37,7 +36,7 @@ impl fmt::Debug for BlockHeaderHash {
|
||||||
|
|
||||||
impl<'a> From<&'a BlockHeader> for BlockHeaderHash {
|
impl<'a> From<&'a BlockHeader> for BlockHeaderHash {
|
||||||
fn from(block_header: &'a BlockHeader) -> Self {
|
fn from(block_header: &'a BlockHeader) -> Self {
|
||||||
let mut hash_writer = Sha256dWriter::default();
|
let mut hash_writer = sha256d::Writer::default();
|
||||||
block_header
|
block_header
|
||||||
.zcash_serialize(&mut hash_writer)
|
.zcash_serialize(&mut hash_writer)
|
||||||
.expect("Sha256dWriter is infallible");
|
.expect("Sha256dWriter is infallible");
|
||||||
|
|
|
@ -4,11 +4,12 @@ use crate::block::{difficulty::CompactDifficulty, light_client::LightClientRootH
|
||||||
use crate::equihash_solution::EquihashSolution;
|
use crate::equihash_solution::EquihashSolution;
|
||||||
use crate::merkle_tree::MerkleTreeRootHash;
|
use crate::merkle_tree::MerkleTreeRootHash;
|
||||||
use crate::serialization::{
|
use crate::serialization::{
|
||||||
SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
|
sha256d, SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
|
||||||
};
|
};
|
||||||
use crate::types::LockTime;
|
use crate::types::LockTime;
|
||||||
use crate::Network;
|
use crate::Network;
|
||||||
use crate::{sha256d_writer::Sha256dWriter, test::generate};
|
|
||||||
|
use crate::test::generate;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc};
|
use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc};
|
||||||
use proptest::{
|
use proptest::{
|
||||||
|
@ -79,7 +80,7 @@ impl Arbitrary for BlockHeader {
|
||||||
#[test]
|
#[test]
|
||||||
fn blockheaderhash_debug() {
|
fn blockheaderhash_debug() {
|
||||||
let preimage = b"foo bar baz";
|
let preimage = b"foo bar baz";
|
||||||
let mut sha_writer = Sha256dWriter::default();
|
let mut sha_writer = sha256d::Writer::default();
|
||||||
let _ = sha_writer.write_all(preimage);
|
let _ = sha_writer.write_all(preimage);
|
||||||
|
|
||||||
let hash = BlockHeaderHash(sha_writer.finish());
|
let hash = BlockHeaderHash(sha_writer.finish());
|
||||||
|
|
|
@ -13,7 +13,6 @@ extern crate serde;
|
||||||
|
|
||||||
mod merkle_tree;
|
mod merkle_tree;
|
||||||
mod serde_helpers;
|
mod serde_helpers;
|
||||||
mod sha256d_writer;
|
|
||||||
|
|
||||||
pub mod addresses;
|
pub mod addresses;
|
||||||
pub mod block;
|
pub mod block;
|
||||||
|
|
|
@ -7,8 +7,7 @@ use std::{fmt, io};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use proptest_derive::Arbitrary;
|
use proptest_derive::Arbitrary;
|
||||||
|
|
||||||
use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize};
|
use crate::serialization::{sha256d, SerializationError, ZcashDeserialize, ZcashSerialize};
|
||||||
use crate::sha256d_writer::Sha256dWriter;
|
|
||||||
use crate::transaction::Transaction;
|
use crate::transaction::Transaction;
|
||||||
|
|
||||||
/// A binary hash tree of SHA256d (two rounds of SHA256) hashes for
|
/// A binary hash tree of SHA256d (two rounds of SHA256) hashes for
|
||||||
|
@ -38,7 +37,7 @@ pub struct MerkleTreeRootHash(pub [u8; 32]);
|
||||||
|
|
||||||
impl From<MerkleTree<Transaction>> for MerkleTreeRootHash {
|
impl From<MerkleTree<Transaction>> for MerkleTreeRootHash {
|
||||||
fn from(merkle_tree: MerkleTree<Transaction>) -> Self {
|
fn from(merkle_tree: MerkleTree<Transaction>) -> Self {
|
||||||
let mut hash_writer = Sha256dWriter::default();
|
let mut hash_writer = sha256d::Writer::default();
|
||||||
merkle_tree
|
merkle_tree
|
||||||
.zcash_serialize(&mut hash_writer)
|
.zcash_serialize(&mut hash_writer)
|
||||||
.expect("Sha256dWriter is infallible");
|
.expect("Sha256dWriter is infallible");
|
||||||
|
|
|
@ -12,6 +12,8 @@ mod write_zcash;
|
||||||
mod zcash_deserialize;
|
mod zcash_deserialize;
|
||||||
mod zcash_serialize;
|
mod zcash_serialize;
|
||||||
|
|
||||||
|
pub mod sha256d;
|
||||||
|
|
||||||
pub use error::SerializationError;
|
pub use error::SerializationError;
|
||||||
pub use read_zcash::ReadZcashExt;
|
pub use read_zcash::ReadZcashExt;
|
||||||
pub use write_zcash::WriteZcashExt;
|
pub use write_zcash::WriteZcashExt;
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
//! SHA256d, a.k.a., double SHA2, a.k.a., 2 SHA 2 Furious
|
||||||
|
|
||||||
|
use std::{fmt, io::prelude::*};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
/// An `io::Write` instance that produces a SHA256d output.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Writer {
|
||||||
|
hash: Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Writer {
|
||||||
|
/// Consume the Writer and produce the hash result.
|
||||||
|
pub fn finish(self) -> [u8; 32] {
|
||||||
|
let result1 = self.hash.result();
|
||||||
|
let result2 = Sha256::digest(&result1);
|
||||||
|
let mut buffer = [0u8; 32];
|
||||||
|
buffer[0..32].copy_from_slice(&result2[0..32]);
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Writer {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.hash.input(buf);
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 4-byte checksum using truncated double-SHA256 (two rounds of SHA256).
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Checksum(pub [u8; 4]);
|
||||||
|
|
||||||
|
impl<'a> From<&'a [u8]> for Checksum {
|
||||||
|
fn from(bytes: &'a [u8]) -> Self {
|
||||||
|
let hash1 = Sha256::digest(bytes);
|
||||||
|
let hash2 = Sha256::digest(&hash1);
|
||||||
|
let mut checksum = [0u8; 4];
|
||||||
|
checksum[0..4].copy_from_slice(&hash2[0..4]);
|
||||||
|
Self(checksum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Checksum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_tuple("Sha256dChecksum")
|
||||||
|
.field(&hex::encode(&self.0))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sha256d_checksum() {
|
||||||
|
// https://en.bitcoin.it/wiki/Protocol_documentation#Hashes
|
||||||
|
let input = b"hello";
|
||||||
|
let checksum = Checksum::from(&input[..]);
|
||||||
|
let expected = Checksum([0x95, 0x95, 0xc9, 0xdf]);
|
||||||
|
assert_eq!(checksum, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sha256d_checksum_debug() {
|
||||||
|
let input = b"hello";
|
||||||
|
let checksum = Checksum::from(&input[..]);
|
||||||
|
|
||||||
|
assert_eq!(format!("{:?}", checksum), "Sha256dChecksum(\"9595c9df\")");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
//! A Writer for Sha256d-related (two rounds of SHA256) types.
|
|
||||||
|
|
||||||
use std::io::prelude::*;
|
|
||||||
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
/// A type that lets you write out SHA256d (double-SHA256, as in two rounds).
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Sha256dWriter {
|
|
||||||
hash: Sha256,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sha256dWriter {
|
|
||||||
/// Consume the Writer and produce the hash result.
|
|
||||||
pub fn finish(self) -> [u8; 32] {
|
|
||||||
let result1 = self.hash.result();
|
|
||||||
let result2 = Sha256::digest(&result1);
|
|
||||||
let mut buffer = [0u8; 32];
|
|
||||||
buffer[0..32].copy_from_slice(&result2[0..32]);
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for Sha256dWriter {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
||||||
self.hash.input(buf);
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> std::io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use crate::sha256d_writer::Sha256dWriter;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// Just checking that `flush()` succeeds without error.
|
|
||||||
fn sha256d_flush() {
|
|
||||||
let preimage = b"foo bar baz";
|
|
||||||
let mut sha_writer = Sha256dWriter::default();
|
|
||||||
let _ = sha_writer.write_all(preimage);
|
|
||||||
|
|
||||||
sha_writer.flush().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,10 +5,7 @@ use std::fmt;
|
||||||
use proptest_derive::Arbitrary;
|
use proptest_derive::Arbitrary;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::serialization::{sha256d, SerializationError, ZcashSerialize};
|
||||||
serialization::{SerializationError, ZcashSerialize},
|
|
||||||
sha256d_writer::Sha256dWriter,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Transaction;
|
use super::Transaction;
|
||||||
|
|
||||||
|
@ -22,7 +19,7 @@ pub struct TransactionHash(pub [u8; 32]);
|
||||||
|
|
||||||
impl From<Transaction> for TransactionHash {
|
impl From<Transaction> for TransactionHash {
|
||||||
fn from(transaction: Transaction) -> Self {
|
fn from(transaction: Transaction) -> Self {
|
||||||
let mut hash_writer = Sha256dWriter::default();
|
let mut hash_writer = sha256d::Writer::default();
|
||||||
transaction
|
transaction
|
||||||
.zcash_serialize(&mut hash_writer)
|
.zcash_serialize(&mut hash_writer)
|
||||||
.expect("Transactions must serialize into the hash.");
|
.expect("Transactions must serialize into the hash.");
|
||||||
|
@ -53,26 +50,8 @@ impl std::str::FromStr for TransactionHash {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use crate::sha256d_writer::Sha256dWriter;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn transactionhash_debug() {
|
|
||||||
let preimage = b"foo bar baz";
|
|
||||||
let mut sha_writer = Sha256dWriter::default();
|
|
||||||
let _ = sha_writer.write_all(preimage);
|
|
||||||
|
|
||||||
let hash = TransactionHash(sha_writer.finish());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{:?}", hash),
|
|
||||||
r#"TransactionHash("bf46b4b5030752fedac6f884976162bbfb29a9398f104a280b3e34d51b416631")"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transactionhash_from_str() {
|
fn transactionhash_from_str() {
|
||||||
let hash: TransactionHash =
|
let hash: TransactionHash =
|
||||||
|
|
|
@ -216,57 +216,11 @@ impl ZcashDeserialize for Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A 4-byte checksum using truncated double-SHA256 (two rounds of SHA256).
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
||||||
pub struct Sha256dChecksum(pub [u8; 4]);
|
|
||||||
|
|
||||||
impl<'a> From<&'a [u8]> for Sha256dChecksum {
|
|
||||||
fn from(bytes: &'a [u8]) -> Self {
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
let hash1 = Sha256::digest(bytes);
|
|
||||||
let hash2 = Sha256::digest(&hash1);
|
|
||||||
let mut checksum = [0u8; 4];
|
|
||||||
checksum[0..4].copy_from_slice(&hash2[0..4]);
|
|
||||||
Self(checksum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Sha256dChecksum {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.debug_tuple("Sha256dChecksum")
|
|
||||||
.field(&hex::encode(&self.0))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sha256d_checksum() {
|
|
||||||
// https://en.bitcoin.it/wiki/Protocol_documentation#Hashes
|
|
||||||
let input = b"hello";
|
|
||||||
let checksum = Sha256dChecksum::from(&input[..]);
|
|
||||||
let expected = Sha256dChecksum([0x95, 0x95, 0xc9, 0xdf]);
|
|
||||||
assert_eq!(checksum, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sha256d_checksum_debug() {
|
|
||||||
let input = b"hello";
|
|
||||||
let checksum = Sha256dChecksum::from(&input[..]);
|
|
||||||
|
|
||||||
assert_eq!(format!("{:?}", checksum), "Sha256dChecksum(\"9595c9df\")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod proptests {
|
mod proptests {
|
||||||
|
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
@ -275,7 +229,6 @@ mod proptests {
|
||||||
use crate::serialization::{ZcashDeserialize, ZcashSerialize};
|
use crate::serialization::{ZcashDeserialize, ZcashSerialize};
|
||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn locktime_roundtrip(locktime in any::<LockTime>()) {
|
fn locktime_roundtrip(locktime in any::<LockTime>()) {
|
||||||
let mut bytes = Cursor::new(Vec::new());
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
@ -297,6 +250,5 @@ mod proptests {
|
||||||
|
|
||||||
prop_assert_eq![script, other_script];
|
prop_assert_eq![script, other_script];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,11 @@ use tokio_util::codec::{Decoder, Encoder};
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{Block, BlockHeaderHash},
|
block::{Block, BlockHeaderHash},
|
||||||
serialization::{
|
serialization::{
|
||||||
ReadZcashExt, SerializationError as Error, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
|
sha256d, ReadZcashExt, SerializationError as Error, WriteZcashExt, ZcashDeserialize,
|
||||||
|
ZcashSerialize,
|
||||||
},
|
},
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
types::{BlockHeight, Sha256dChecksum},
|
types::BlockHeight,
|
||||||
Network,
|
Network,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -162,7 +163,7 @@ impl Encoder for Codec {
|
||||||
header_writer.write_all(&Magic::from(self.builder.network).0[..])?;
|
header_writer.write_all(&Magic::from(self.builder.network).0[..])?;
|
||||||
header_writer.write_all(command)?;
|
header_writer.write_all(command)?;
|
||||||
header_writer.write_u32::<LittleEndian>(body.len() as u32)?;
|
header_writer.write_u32::<LittleEndian>(body.len() as u32)?;
|
||||||
header_writer.write_all(&Sha256dChecksum::from(&body[..]).0)?;
|
header_writer.write_all(&sha256d::Checksum::from(&body[..]).0)?;
|
||||||
|
|
||||||
dst.reserve(HEADER_LEN + body.len());
|
dst.reserve(HEADER_LEN + body.len());
|
||||||
dst.extend_from_slice(&header);
|
dst.extend_from_slice(&header);
|
||||||
|
@ -276,7 +277,7 @@ enum DecodeState {
|
||||||
Body {
|
Body {
|
||||||
body_len: usize,
|
body_len: usize,
|
||||||
command: [u8; 12],
|
command: [u8; 12],
|
||||||
checksum: Sha256dChecksum,
|
checksum: sha256d::Checksum,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +322,7 @@ impl Decoder for Codec {
|
||||||
let magic = Magic(header_reader.read_4_bytes()?);
|
let magic = Magic(header_reader.read_4_bytes()?);
|
||||||
let command = header_reader.read_12_bytes()?;
|
let command = header_reader.read_12_bytes()?;
|
||||||
let body_len = header_reader.read_u32::<LittleEndian>()? as usize;
|
let body_len = header_reader.read_u32::<LittleEndian>()? as usize;
|
||||||
let checksum = Sha256dChecksum(header_reader.read_4_bytes()?);
|
let checksum = sha256d::Checksum(header_reader.read_4_bytes()?);
|
||||||
trace!(
|
trace!(
|
||||||
?self.state,
|
?self.state,
|
||||||
?magic,
|
?magic,
|
||||||
|
@ -371,7 +372,7 @@ impl Decoder for Codec {
|
||||||
let body = src.split_to(body_len);
|
let body = src.split_to(body_len);
|
||||||
self.state = DecodeState::Head;
|
self.state = DecodeState::Head;
|
||||||
|
|
||||||
if checksum != Sha256dChecksum::from(&body[..]) {
|
if checksum != sha256d::Checksum::from(&body[..]) {
|
||||||
return Err(Parse(
|
return Err(Parse(
|
||||||
"supplied message checksum does not match computed checksum",
|
"supplied message checksum does not match computed checksum",
|
||||||
));
|
));
|
||||||
|
@ -691,29 +692,6 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn decode_state_debug() {
|
|
||||||
assert_eq!(format!("{:?}", DecodeState::Head), "DecodeState::Head");
|
|
||||||
|
|
||||||
let decode_state = DecodeState::Body {
|
|
||||||
body_len: 43,
|
|
||||||
command: [118, 101, 114, 115, 105, 111, 110, 0, 0, 0, 0, 0],
|
|
||||||
checksum: Sha256dChecksum([186, 250, 162, 227]),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(format!("{:?}", decode_state),
|
|
||||||
"DecodeState::Body { body_len: 43, command: \"version\\u{0}\\u{0}\\u{0}\\u{0}\\u{0}\", checksum: Sha256dChecksum(\"bafaa2e3\") }");
|
|
||||||
|
|
||||||
let decode_state = DecodeState::Body {
|
|
||||||
body_len: 43,
|
|
||||||
command: [118, 240, 144, 128, 105, 111, 110, 0, 0, 0, 0, 0],
|
|
||||||
checksum: Sha256dChecksum([186, 250, 162, 227]),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(format!("{:?}", decode_state),
|
|
||||||
"DecodeState::Body { body_len: 43, command: \"v<EFBFBD>ion\\u{0}\\u{0}\\u{0}\\u{0}\\u{0}\", checksum: Sha256dChecksum(\"bafaa2e3\") }");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn max_msg_size_round_trip() {
|
fn max_msg_size_round_trip() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
Loading…
Reference in New Issue