Merge rust-bitcoin/rust-bitcoin#3194: priority: Prepare moving script types to `primitives`

8f2f4cbb3c Re-order optional dependencies (Tobin C. Harding)
95f2a8dab6 Do not access ScriptBuf inner from builder (Tobin C. Harding)
900af453ff Stop accessing inner ScriptBuf field when encoding (Tobin C. Harding)
8b82363d97 Use Script::as_bytes instead of inner when indexing (Tobin C. Harding)
b0675a4a4f Use Script::len instead of inner field (Tobin C. Harding)
374c6118dc Deprecate Script::fmt_asm and to_asm_str (Tobin C. Harding)

Pull request description:

  Move the `Script` and `ScriptBuf` types to `primitives`. There were still a few preparations required, things we had missed while creating the extension traits.

  Note also please, in the last patch, we enable `hex` from the `serder` feature. This is not the final state we want but like we did for `alloc` it is a step to reduce the size of the diff.

ACKs for top commit:
  Kixunil:
    ACK 8f2f4cbb3c
  apoelstra:
    ACK 8f2f4cbb3c successfully ran local tests

Tree-SHA512: 62a5f3c253ecb54d95c37fdc7eb955f3952909dc3bca20444b85c44665f54d5a0c48daf729bed0dd60ff3e9571b41deed039984c8b757b075ac6e136cacd17d7
This commit is contained in:
merge-script 2024-09-13 16:58:59 +00:00
commit 60e15b8007
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
6 changed files with 49 additions and 48 deletions

View File

@ -35,13 +35,12 @@ primitives = { package = "bitcoin-primitives", version = "0.100.0", default-feat
secp256k1 = { version = "0.29.0", default-features = false, features = ["hashes", "alloc"] } secp256k1 = { version = "0.29.0", default-features = false, features = ["hashes", "alloc"] }
units = { package = "bitcoin-units", version = "0.1.0", default-features = false, features = ["alloc"] } units = { package = "bitcoin-units", version = "0.1.0", default-features = false, features = ["alloc"] }
arbitrary = { version = "1", optional = true }
base64 = { version = "0.22.0", optional = true } base64 = { version = "0.22.0", optional = true }
ordered = { version = "0.2.0", optional = true }
# `bitcoinconsensus` version includes metadata which indicates the version of Core. Use `cargo tree` to see it. # `bitcoinconsensus` version includes metadata which indicates the version of Core. Use `cargo tree` to see it.
bitcoinconsensus = { version = "0.106.0", default-features = false, optional = true } bitcoinconsensus = { version = "0.106.0", default-features = false, optional = true }
ordered = { version = "0.2.0", optional = true }
serde = { version = "1.0.103", default-features = false, features = [ "derive", "alloc" ], optional = true } serde = { version = "1.0.103", default-features = false, features = [ "derive", "alloc" ], optional = true }
arbitrary = { version = "1", optional = true }
[dev-dependencies] [dev-dependencies]
internals = { package = "bitcoin-internals", version = "0.3.0", features = ["test-serde"] } internals = { package = "bitcoin-internals", version = "0.3.0", features = ["test-serde"] }

View File

@ -175,13 +175,13 @@ crate::internal_macros::define_extension_trait! {
/// > byte vector pushed is called the "witness program". /// > byte vector pushed is called the "witness program".
#[inline] #[inline]
fn witness_version(&self) -> Option<WitnessVersion> { fn witness_version(&self) -> Option<WitnessVersion> {
let script_len = self.0.len(); let script_len = self.len();
if !(4..=42).contains(&script_len) { if !(4..=42).contains(&script_len) {
return None; return None;
} }
let ver_opcode = Opcode::from(self.0[0]); // Version 0 or PUSHNUM_1-PUSHNUM_16 let ver_opcode = Opcode::from(self.as_bytes()[0]); // Version 0 or PUSHNUM_1-PUSHNUM_16
let push_opbyte = self.0[1]; // Second byte push opcode 2-40 bytes let push_opbyte = self.as_bytes()[1]; // Second byte push opcode 2-40 bytes
if push_opbyte < OP_PUSHBYTES_2.to_u8() || push_opbyte > OP_PUSHBYTES_40.to_u8() { if push_opbyte < OP_PUSHBYTES_2.to_u8() || push_opbyte > OP_PUSHBYTES_40.to_u8() {
return None; return None;
@ -197,21 +197,21 @@ crate::internal_macros::define_extension_trait! {
/// Checks whether a script pubkey is a P2SH output. /// Checks whether a script pubkey is a P2SH output.
#[inline] #[inline]
fn is_p2sh(&self) -> bool { fn is_p2sh(&self) -> bool {
self.0.len() == 23 self.len() == 23
&& self.0[0] == OP_HASH160.to_u8() && self.as_bytes()[0] == OP_HASH160.to_u8()
&& self.0[1] == OP_PUSHBYTES_20.to_u8() && self.as_bytes()[1] == OP_PUSHBYTES_20.to_u8()
&& self.0[22] == OP_EQUAL.to_u8() && self.as_bytes()[22] == OP_EQUAL.to_u8()
} }
/// Checks whether a script pubkey is a P2PKH output. /// Checks whether a script pubkey is a P2PKH output.
#[inline] #[inline]
fn is_p2pkh(&self) -> bool { fn is_p2pkh(&self) -> bool {
self.0.len() == 25 self.len() == 25
&& self.0[0] == OP_DUP.to_u8() && self.as_bytes()[0] == OP_DUP.to_u8()
&& self.0[1] == OP_HASH160.to_u8() && self.as_bytes()[1] == OP_HASH160.to_u8()
&& self.0[2] == OP_PUSHBYTES_20.to_u8() && self.as_bytes()[2] == OP_PUSHBYTES_20.to_u8()
&& self.0[23] == OP_EQUALVERIFY.to_u8() && self.as_bytes()[23] == OP_EQUALVERIFY.to_u8()
&& self.0[24] == OP_CHECKSIG.to_u8() && self.as_bytes()[24] == OP_CHECKSIG.to_u8()
} }
/// Checks whether a script is push only. /// Checks whether a script is push only.
@ -293,25 +293,25 @@ crate::internal_macros::define_extension_trait! {
/// Checks whether a script pubkey is a P2WSH output. /// Checks whether a script pubkey is a P2WSH output.
#[inline] #[inline]
fn is_p2wsh(&self) -> bool { fn is_p2wsh(&self) -> bool {
self.0.len() == 34 self.len() == 34
&& self.witness_version() == Some(WitnessVersion::V0) && self.witness_version() == Some(WitnessVersion::V0)
&& self.0[1] == OP_PUSHBYTES_32.to_u8() && self.as_bytes()[1] == OP_PUSHBYTES_32.to_u8()
} }
/// Checks whether a script pubkey is a P2WPKH output. /// Checks whether a script pubkey is a P2WPKH output.
#[inline] #[inline]
fn is_p2wpkh(&self) -> bool { fn is_p2wpkh(&self) -> bool {
self.0.len() == 22 self.len() == 22
&& self.witness_version() == Some(WitnessVersion::V0) && self.witness_version() == Some(WitnessVersion::V0)
&& self.0[1] == OP_PUSHBYTES_20.to_u8() && self.as_bytes()[1] == OP_PUSHBYTES_20.to_u8()
} }
/// Checks whether a script pubkey is a P2TR output. /// Checks whether a script pubkey is a P2TR output.
#[inline] #[inline]
fn is_p2tr(&self) -> bool { fn is_p2tr(&self) -> bool {
self.0.len() == 34 self.len() == 34
&& self.witness_version() == Some(WitnessVersion::V1) && self.witness_version() == Some(WitnessVersion::V1)
&& self.0[1] == OP_PUSHBYTES_32.to_u8() && self.as_bytes()[1] == OP_PUSHBYTES_32.to_u8()
} }
/// Check if this is a consensus-valid OP_RETURN output. /// Check if this is a consensus-valid OP_RETURN output.
@ -320,7 +320,7 @@ crate::internal_macros::define_extension_trait! {
/// [`is_standard_op_return()`](Self::is_standard_op_return) instead. /// [`is_standard_op_return()`](Self::is_standard_op_return) instead.
#[inline] #[inline]
fn is_op_return(&self) -> bool { fn is_op_return(&self) -> bool {
match self.0.first() { match self.as_bytes().first() {
Some(b) => *b == OP_RETURN.to_u8(), Some(b) => *b == OP_RETURN.to_u8(),
None => false, None => false,
} }
@ -331,7 +331,7 @@ crate::internal_macros::define_extension_trait! {
/// What this function considers to be standard may change without warning pending Bitcoin Core /// What this function considers to be standard may change without warning pending Bitcoin Core
/// changes. /// changes.
#[inline] #[inline]
fn is_standard_op_return(&self) -> bool { self.is_op_return() && self.0.len() <= 80 } fn is_standard_op_return(&self) -> bool { self.is_op_return() && self.len() <= 80 }
/// Checks whether a script is trivially known to have no satisfying input. /// Checks whether a script is trivially known to have no satisfying input.
/// ///
@ -345,7 +345,7 @@ crate::internal_macros::define_extension_trait! {
fn is_provably_unspendable(&self) -> bool { fn is_provably_unspendable(&self) -> bool {
use crate::opcodes::Class::{IllegalOp, ReturnOp}; use crate::opcodes::Class::{IllegalOp, ReturnOp};
match self.0.first() { match self.as_bytes().first() {
Some(b) => { Some(b) => {
let first = Opcode::from(*b); let first = Opcode::from(*b);
let class = first.classify(opcodes::ClassifyContext::Legacy); let class = first.classify(opcodes::ClassifyContext::Legacy);
@ -447,7 +447,7 @@ crate::internal_macros::define_extension_trait! {
/// To force minimal pushes, use [`instructions_minimal`](Self::instructions_minimal). /// To force minimal pushes, use [`instructions_minimal`](Self::instructions_minimal).
#[inline] #[inline]
fn instructions(&self) -> Instructions { fn instructions(&self) -> Instructions {
Instructions { data: self.0.iter(), enforce_minimal: false } Instructions { data: self.as_bytes().iter(), enforce_minimal: false }
} }
/// Iterates over the script instructions while enforcing minimal pushes. /// Iterates over the script instructions while enforcing minimal pushes.
@ -456,7 +456,7 @@ crate::internal_macros::define_extension_trait! {
/// is not minimal. /// is not minimal.
#[inline] #[inline]
fn instructions_minimal(&self) -> Instructions { fn instructions_minimal(&self) -> Instructions {
Instructions { data: self.0.iter(), enforce_minimal: true } Instructions { data: self.as_bytes().iter(), enforce_minimal: true }
} }
/// Iterates over the script instructions and their indices. /// Iterates over the script instructions and their indices.
@ -481,14 +481,16 @@ crate::internal_macros::define_extension_trait! {
} }
/// Writes the human-readable assembly representation of the script to the formatter. /// Writes the human-readable assembly representation of the script to the formatter.
#[deprecated(since = "TBD", note = "use the script's Display impl instead")]
fn fmt_asm(&self, f: &mut dyn fmt::Write) -> fmt::Result { fn fmt_asm(&self, f: &mut dyn fmt::Write) -> fmt::Result {
bytes_to_asm_fmt(self.as_ref(), f) bytes_to_asm_fmt(self.as_ref(), f)
} }
/// Returns the human-readable assembly representation of the script. /// Returns the human-readable assembly representation of the script.
#[deprecated(since = "TBD", note = "use `to_string()` instead")]
fn to_asm_string(&self) -> String { fn to_asm_string(&self) -> String {
let mut buf = String::new(); let mut buf = String::new();
self.fmt_asm(&mut buf).unwrap(); bytes_to_asm_fmt(self.as_ref(), &mut buf).expect("in-memory writers don't fail");
buf buf
} }

View File

@ -7,7 +7,7 @@ use crate::locktime::absolute;
use crate::opcodes::all::*; use crate::opcodes::all::*;
use crate::opcodes::{self, Opcode}; use crate::opcodes::{self, Opcode};
use crate::prelude::Vec; use crate::prelude::Vec;
use crate::script::{ScriptBufExt as _, ScriptExt as _, ScriptExtPriv as _}; use crate::script::{ScriptBufExt as _, ScriptBufExtPriv as _, ScriptExtPriv as _};
use crate::Sequence; use crate::Sequence;
/// An Object which can be used to construct a script piece by piece. /// An Object which can be used to construct a script piece by piece.
@ -82,7 +82,7 @@ impl Builder {
// "duplicated code" because we need to update `1` field // "duplicated code" because we need to update `1` field
match opcode_to_verify(self.1) { match opcode_to_verify(self.1) {
Some(opcode) => { Some(opcode) => {
(self.0).0.pop(); (self.0).as_byte_vec().pop();
self.push_opcode(opcode) self.push_opcode(opcode)
} }
None => self.push_opcode(OP_VERIFY), None => self.push_opcode(OP_VERIFY),
@ -126,7 +126,7 @@ impl From<Vec<u8>> for Builder {
} }
impl fmt::Display for Builder { impl fmt::Display for Builder {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt_asm(f) } fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
} }
internals::debug_from_display!(Builder); internals::debug_from_display!(Builder);

View File

@ -415,7 +415,7 @@ impl AsMut<[u8]> for ScriptBuf {
impl fmt::Debug for Script { impl fmt::Debug for Script {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Script(")?; f.write_str("Script(")?;
self.fmt_asm(f)?; bytes_to_asm_fmt(self.as_ref(), f)?;
f.write_str(")") f.write_str(")")
} }
} }
@ -426,7 +426,7 @@ impl fmt::Debug for ScriptBuf {
impl fmt::Display for Script { impl fmt::Display for Script {
#[inline] #[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_asm(f) } fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { bytes_to_asm_fmt(self.as_ref(), f) }
} }
impl fmt::Display for ScriptBuf { impl fmt::Display for ScriptBuf {
@ -616,14 +616,14 @@ impl<'de> serde::Deserialize<'de> for ScriptBuf {
impl Encodable for Script { impl Encodable for Script {
#[inline] #[inline]
fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> { fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
crate::consensus::encode::consensus_encode_with_size(&self.0, w) crate::consensus::encode::consensus_encode_with_size(self.as_bytes(), w)
} }
} }
impl Encodable for ScriptBuf { impl Encodable for ScriptBuf {
#[inline] #[inline]
fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> { fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
self.0.consensus_encode(w) self.as_script().consensus_encode(w)
} }
} }
@ -632,7 +632,8 @@ impl Decodable for ScriptBuf {
fn consensus_decode_from_finite_reader<R: BufRead + ?Sized>( fn consensus_decode_from_finite_reader<R: BufRead + ?Sized>(
r: &mut R, r: &mut R,
) -> Result<Self, encode::Error> { ) -> Result<Self, encode::Error> {
Ok(ScriptBuf(Decodable::consensus_decode_from_finite_reader(r)?)) let v: Vec<u8> = Decodable::consensus_decode_from_finite_reader(r)?;
Ok(ScriptBuf::from_bytes(v))
} }
} }

View File

@ -483,40 +483,40 @@ fn script_json_serialize() {
#[test] #[test]
fn script_asm() { fn script_asm() {
assert_eq!( assert_eq!(
ScriptBuf::from_hex("6363636363686868686800").unwrap().to_asm_string(), ScriptBuf::from_hex("6363636363686868686800").unwrap().to_string(),
"OP_IF OP_IF OP_IF OP_IF OP_IF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_0" "OP_IF OP_IF OP_IF OP_IF OP_IF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_0"
); );
assert_eq!( assert_eq!(
ScriptBuf::from_hex("6363636363686868686800").unwrap().to_asm_string(), ScriptBuf::from_hex("6363636363686868686800").unwrap().to_string(),
"OP_IF OP_IF OP_IF OP_IF OP_IF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_0" "OP_IF OP_IF OP_IF OP_IF OP_IF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_0"
); );
assert_eq!(ScriptBuf::from_hex("2102715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699ac").unwrap().to_asm_string(), assert_eq!(ScriptBuf::from_hex("2102715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699ac").unwrap().to_string(),
"OP_PUSHBYTES_33 02715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699 OP_CHECKSIG"); "OP_PUSHBYTES_33 02715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699 OP_CHECKSIG");
// Elements Alpha peg-out transaction with some signatures removed for brevity. Mainly to test PUSHDATA1 // Elements Alpha peg-out transaction with some signatures removed for brevity. Mainly to test PUSHDATA1
assert_eq!(ScriptBuf::from_hex("0047304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401004cf1552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae").unwrap().to_asm_string(), assert_eq!(ScriptBuf::from_hex("0047304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401004cf1552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae").unwrap().to_string(),
"OP_0 OP_PUSHBYTES_71 304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401 OP_0 OP_PUSHDATA1 552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae"); "OP_0 OP_PUSHBYTES_71 304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401 OP_0 OP_PUSHDATA1 552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae");
// Various weird scripts found in transaction 6d7ed9914625c73c0288694a6819196a27ef6c08f98e1270d975a8e65a3dc09a // Various weird scripts found in transaction 6d7ed9914625c73c0288694a6819196a27ef6c08f98e1270d975a8e65a3dc09a
// which triggerred overflow bugs on 32-bit machines in script formatting in the past. // which triggerred overflow bugs on 32-bit machines in script formatting in the past.
assert_eq!( assert_eq!(
ScriptBuf::from_hex("01").unwrap().to_asm_string(), ScriptBuf::from_hex("01").unwrap().to_string(),
"OP_PUSHBYTES_1 <push past end>" "OP_PUSHBYTES_1 <push past end>"
); );
assert_eq!( assert_eq!(
ScriptBuf::from_hex("0201").unwrap().to_asm_string(), ScriptBuf::from_hex("0201").unwrap().to_string(),
"OP_PUSHBYTES_2 <push past end>" "OP_PUSHBYTES_2 <push past end>"
); );
assert_eq!(ScriptBuf::from_hex("4c").unwrap().to_asm_string(), "<unexpected end>"); assert_eq!(ScriptBuf::from_hex("4c").unwrap().to_string(), "<unexpected end>");
assert_eq!( assert_eq!(
ScriptBuf::from_hex("4c0201").unwrap().to_asm_string(), ScriptBuf::from_hex("4c0201").unwrap().to_string(),
"OP_PUSHDATA1 <push past end>" "OP_PUSHDATA1 <push past end>"
); );
assert_eq!(ScriptBuf::from_hex("4d").unwrap().to_asm_string(), "<unexpected end>"); assert_eq!(ScriptBuf::from_hex("4d").unwrap().to_string(), "<unexpected end>");
assert_eq!( assert_eq!(
ScriptBuf::from_hex("4dffff01").unwrap().to_asm_string(), ScriptBuf::from_hex("4dffff01").unwrap().to_string(),
"OP_PUSHDATA2 <push past end>" "OP_PUSHDATA2 <push past end>"
); );
assert_eq!( assert_eq!(
ScriptBuf::from_hex("4effffffff01").unwrap().to_asm_string(), ScriptBuf::from_hex("4effffffff01").unwrap().to_string(),
"OP_PUSHDATA4 <push past end>" "OP_PUSHDATA4 <push past end>"
); );
} }

View File

@ -27,7 +27,6 @@ io = { package = "bitcoin-io", version = "0.1.1", default-features = false }
units = { package = "bitcoin-units", version = "0.1.0", default-features = false } units = { package = "bitcoin-units", version = "0.1.0", default-features = false }
ordered = { version = "0.2.0", optional = true } ordered = { version = "0.2.0", optional = true }
serde = { version = "1.0.103", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0.103", default-features = false, features = ["derive"], optional = true }
[dev-dependencies] [dev-dependencies]