From cd441382fafa84cda9b456730b9ac42f6b0c9474 Mon Sep 17 00:00:00 2001 From: Tobin Harding Date: Wed, 16 Feb 2022 16:30:47 +0000 Subject: [PATCH] Use fixed width serde impls for keys Currently we serialize keys using the `BytesVisitor`, this causes the serialized data to contain additional metadata encoding the length (an extra 8 bytes). This extra data is unnecessary since we know in advance the length of these two types. Implement a sequence based visitor that encodes the keys as fixed width data. Do fixed width serde imples for: - SecretKey - PublicKey - KeyPair - XOnlyPublicKey --- Cargo.toml | 1 + contrib/test.sh | 14 +++-- no_std_test/Cargo.toml | 3 +- no_std_test/src/main.rs | 47 ++++++++------- src/key.rs | 124 +++++++++++++++++++++++++++++++--------- src/lib.rs | 3 + src/schnorr.rs | 11 +++- src/serde_util.rs | 54 +++++++++++++++++ 8 files changed, 200 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8d55c1883..515c9edcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ global-context-less-secure = [] secp256k1-sys = { version = "0.4.2", default-features = false, path = "./secp256k1-sys" } bitcoin_hashes = { version = "0.10", optional = true } rand = { version = "0.6", default-features = false, optional = true } +# Our custom serde code requires an allocator, enable secp256k1 feature `std` or `alloc`. serde = { version = "1.0", default-features = false, optional = true } diff --git a/contrib/test.sh b/contrib/test.sh index c79d3f8ea..6b8469c45 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -1,7 +1,7 @@ #!/bin/sh -ex # TODO: Add "alloc" once we bump MSRV to past 1.29 -FEATURES="bitcoin_hashes global-context lowmemory rand rand-std recovery serde std" +FEATURES="bitcoin_hashes global-context lowmemory rand rand-std recovery std" # Use toolchain if explicitly specified if [ -n "$TOOLCHAIN" ] @@ -24,8 +24,8 @@ if [ "$DO_FEATURE_MATRIX" = true ]; then cargo test --all --no-default-features # All features - cargo build --all --no-default-features --features="$FEATURES" - cargo test --all --no-default-features --features="$FEATURES" + cargo build --all --no-default-features --features="$FEATURES serde" + cargo test --all --no-default-features --features="$FEATURES serde" # Single features for feature in ${FEATURES} do @@ -35,9 +35,12 @@ if [ "$DO_FEATURE_MATRIX" = true ]; then # Other combos RUSTFLAGS='--cfg=fuzzing' RUSTDOCFLAGS=$RUSTFLAGS cargo test --all - RUSTFLAGS='--cfg=fuzzing' RUSTDOCFLAGS=$RUSTFLAGS cargo test --all --features="$FEATURES" + RUSTFLAGS='--cfg=fuzzing' RUSTDOCFLAGS=$RUSTFLAGS cargo test --all --features="$FEATURES serde" cargo test --all --features="rand rand-std" cargo test --all --features="rand serde" + # serde requires an allocator + cargo build --all --no-default-features --features=alloc,serde + cargo test --all --no-default-features --features=alloc,serde if [ "$DO_BENCH" = true ]; then # proxy for us having a nightly compiler cargo test --all --all-features @@ -52,7 +55,7 @@ fi # Docs if [ "$DO_DOCS" = true ]; then - cargo doc --all --features="$FEATURES" + cargo doc --all --features="$FEATURES serde" fi # Webassembly stuff @@ -77,6 +80,7 @@ if [ "$DO_ASAN" = true ]; then cargo test --lib --all --features="$FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu && cargo run --release --manifest-path=./no_std_test/Cargo.toml | grep -q "Verified Successfully" cargo run --release --features=alloc --manifest-path=./no_std_test/Cargo.toml | grep -q "Verified alloc Successfully" + cargo run --release --features=use-serde --manifest-path=./no_std_test/Cargo.toml | grep -q "Verified Successfully" fi # Test if panic in C code aborts the process (either with a real panic or with SIGILL) diff --git a/no_std_test/Cargo.toml b/no_std_test/Cargo.toml index d36523550..b4a9ad4ac 100644 --- a/no_std_test/Cargo.toml +++ b/no_std_test/Cargo.toml @@ -5,10 +5,11 @@ authors = ["Elichai Turkel "] [features] alloc = ["secp256k1/alloc", "wee_alloc"] +use-serde = ["alloc", "secp256k1/serde"] [dependencies] wee_alloc = { version = "0.4.5", optional = true } -secp256k1 = { path = "../", default-features = false, features = ["serde", "rand", "recovery"] } +secp256k1 = { path = "../", default-features = false, features = ["rand", "recovery"] } libc = { version = "0.2", default-features = false } serde_cbor = { version = "0.10", default-features = false } # A random serializer that supports no-std. diff --git a/no_std_test/src/main.rs b/no_std_test/src/main.rs index 68cdd9184..26f803cfc 100644 --- a/no_std_test/src/main.rs +++ b/no_std_test/src/main.rs @@ -65,16 +65,10 @@ use core::fmt::{self, write, Write}; use core::intrinsics; use core::panic::PanicInfo; -use secp256k1::ecdh::SharedSecret; use secp256k1::ffi::types::AlignedType; use secp256k1::rand::{self, RngCore}; -use secp256k1::serde::Serialize; use secp256k1::*; -use serde_cbor::de; -use serde_cbor::ser::SliceWrite; -use serde_cbor::Serializer; - struct FakeRng; impl RngCore for FakeRng { fn next_u32(&mut self) -> u32 { @@ -116,25 +110,34 @@ fn start(_argc: isize, _argv: *const *const u8) -> isize { let new_rec_sig = ecdsa::RecoverableSignature::from_compact(&data, rec_id).unwrap(); assert_eq!(rec_sig, new_rec_sig); - let mut cbor_ser = [0u8; 100]; - let writer = SliceWrite::new(&mut cbor_ser[..]); - let mut ser = Serializer::new(writer); - sig.serialize(&mut ser).unwrap(); - let size = ser.into_inner().bytes_written(); - let new_sig: ecdsa::Signature = de::from_mut_slice(&mut cbor_ser[..size]).unwrap(); - assert_eq!(sig, new_sig); - - let _ = SharedSecret::new(&public_key, &secret_key); - let mut x_arr = [0u8; 32]; - let y_arr = SharedSecret::new_with_hash(&public_key, &secret_key, |x,y| { - x_arr = x; - y.into() - }); - assert_ne!(x_arr, [0u8; 32]); - assert_ne!(&y_arr[..], &[0u8; 32][..]); + #[cfg(feature = "use-serde")] { + use secp256k1::serde::Serialize; + use serde_cbor::de; + use serde_cbor::ser::SliceWrite; + use serde_cbor::Serializer; + + let mut cbor_ser = [0u8; 100]; + let writer = SliceWrite::new(&mut cbor_ser[..]); + let mut ser = Serializer::new(writer); + sig.serialize(&mut ser).unwrap(); + let size = ser.into_inner().bytes_written(); + let new_sig: ecdsa::Signature = de::from_mut_slice(&mut cbor_ser[..size]).unwrap(); + assert_eq!(sig, new_sig); + } #[cfg(feature = "alloc")] { + use secp256k1::ecdh::SharedSecret; + + let _ = SharedSecret::new(&public_key, &secret_key); + let mut x_arr = [0u8; 32]; + let y_arr = SharedSecret::new_with_hash(&public_key, &secret_key, |x,y| { + x_arr = x; + y.into() + }); + assert_ne!(x_arr, [0u8; 32]); + assert_ne!(&y_arr[..], &[0u8; 32][..]); + let secp_alloc = Secp256k1::new(); let public_key = PublicKey::from_secret_key(&secp_alloc, &secret_key); let message = Message::from_slice(&[0xab; 32]).expect("32 bytes"); diff --git a/src/key.rs b/src/key.rs index 7f77d4115..73364c628 100644 --- a/src/key.rs +++ b/src/key.rs @@ -28,6 +28,9 @@ use Verification; use constants; use ffi::{self, CPtr}; +#[cfg(feature = "serde")] +use serde::ser::SerializeTuple; + #[cfg(feature = "global-context")] use {Message, ecdsa, SECP256K1}; #[cfg(all(feature = "global-context", feature = "rand-std"))] @@ -302,13 +305,17 @@ impl ::serde::Serialize for SecretKey { let mut buf = [0u8; 64]; s.serialize_str(::to_hex(&self.0, &mut buf).expect("fixed-size hex serialization")) } else { - s.serialize_bytes(&self[..]) + let mut seq = s.serialize_tuple(constants::SECRET_KEY_SIZE)?; + for byte in self.0.iter() { + seq.serialize_element(byte)?; + } + seq.end() } } } -#[cfg(feature = "serde")] -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))))] impl<'de> ::serde::Deserialize<'de> for SecretKey { fn deserialize>(d: D) -> Result { if d.is_human_readable() { @@ -316,7 +323,7 @@ impl<'de> ::serde::Deserialize<'de> for SecretKey { "a hex string representing 32 byte SecretKey" )) } else { - d.deserialize_bytes(super::serde_util::BytesVisitor::new( + d.deserialize_seq(super::serde_util::SeqVisitor::new( "raw 32 bytes SecretKey", SecretKey::from_slice )) @@ -615,16 +622,21 @@ impl From for PublicKey { #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] impl ::serde::Serialize for PublicKey { fn serialize(&self, s: S) -> Result { + // FIXME: human_readable serializes the whole 64 bytes but non-human-readable does 33 (compressed). Is this correct? if s.is_human_readable() { s.collect_str(self) } else { - s.serialize_bytes(&self.serialize()) + let mut seq = s.serialize_tuple(constants::PUBLIC_KEY_SIZE)?; + for byte in self.serialize().iter() { // Serialize in compressed form. + seq.serialize_element(&byte)?; + } + seq.end() } } } -#[cfg(feature = "serde")] -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))))] impl<'de> ::serde::Deserialize<'de> for PublicKey { fn deserialize>(d: D) -> Result { if d.is_human_readable() { @@ -632,7 +644,7 @@ impl<'de> ::serde::Deserialize<'de> for PublicKey { "an ASCII hex string representing a public key" )) } else { - d.deserialize_bytes(super::serde_util::BytesVisitor::new( + d.deserialize_seq(super::serde_util::SeqVisitor::new( "a bytestring representing a public key", PublicKey::from_slice )) @@ -929,13 +941,17 @@ impl ::serde::Serialize for KeyPair { s.serialize_str(::to_hex(&self.serialize_secret(), &mut buf) .expect("fixed-size hex serialization")) } else { - s.serialize_bytes(&self.0[..]) + let mut seq = s.serialize_tuple(constants::SECRET_KEY_SIZE)?; + for byte in self.serialize_secret().iter() { + seq.serialize_element(&byte)?; + } + seq.end() } } } -#[cfg(feature = "serde")] -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))))] impl<'de> ::serde::Deserialize<'de> for KeyPair { fn deserialize>(d: D) -> Result { if d.is_human_readable() { @@ -943,7 +959,7 @@ impl<'de> ::serde::Deserialize<'de> for KeyPair { "a hex string representing 32 byte KeyPair" )) } else { - d.deserialize_bytes(super::serde_util::BytesVisitor::new( + d.deserialize_seq(super::serde_util::SeqVisitor::new( "raw 32 bytes KeyPair", |data| unsafe { let ctx = Secp256k1::from_raw_all(ffi::secp256k1_context_no_precomp as *mut ffi::Context); @@ -1350,16 +1366,21 @@ impl From<::key::PublicKey> for XOnlyPublicKey { #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] impl ::serde::Serialize for XOnlyPublicKey { fn serialize(&self, s: S) -> Result { + // FIXME: human_readable serializes the whole 64 bytes but non-human-readable does 32 bytes (x co-ordinate). Is this correct? if s.is_human_readable() { s.collect_str(self) } else { - s.serialize_bytes(&self.serialize()) + let mut seq = s.serialize_tuple(constants::SCHNORRSIG_PUBLIC_KEY_SIZE)?; + for byte in self.serialize().iter() { + seq.serialize_element(&byte)?; + } + seq.end() } } } -#[cfg(feature = "serde")] -#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))))] impl<'de> ::serde::Deserialize<'de> for XOnlyPublicKey { fn deserialize>(d: D) -> Result { if d.is_human_readable() { @@ -1367,7 +1388,7 @@ impl<'de> ::serde::Deserialize<'de> for XOnlyPublicKey { "a hex string representing 32 byte schnorr public key" )) } else { - d.deserialize_bytes(super::serde_util::BytesVisitor::new( + d.deserialize_seq(super::serde_util::SeqVisitor::new( "raw 32 bytes schnorr public key", XOnlyPublicKey::from_slice )) @@ -1911,6 +1932,7 @@ mod test { static SK_STR: &'static str = "\ 01010101010101010001020304050607ffff0000ffff00006363636363636363\ "; + #[cfg(fuzzing)] static PK_BYTES: [u8; 33] = [ 0x02, 0x18, 0x84, 0x57, 0x81, 0xf6, 0x31, 0xc4, 0x8f, @@ -1933,22 +1955,32 @@ mod test { #[cfg(fuzzing)] let pk = PublicKey::from_slice(&PK_BYTES).expect("pk"); - assert_tokens(&sk.compact(), &[Token::BorrowedBytes(&SK_BYTES[..])]); - assert_tokens(&sk.compact(), &[Token::Bytes(&SK_BYTES)]); - assert_tokens(&sk.compact(), &[Token::ByteBuf(&SK_BYTES)]); + assert_tokens(&sk.compact(), &[ + Token::Tuple{ len: 32 }, + Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), + Token::U8(0), Token::U8(1), Token::U8(2), Token::U8(3), Token::U8(4), Token::U8(5), Token::U8(6), Token::U8(7), + Token::U8(0xff), Token::U8(0xff), Token::U8(0), Token::U8(0), Token::U8(0xff), Token::U8(0xff), Token::U8(0), Token::U8(0), + Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), + Token::TupleEnd + ]); assert_tokens(&sk.readable(), &[Token::BorrowedStr(SK_STR)]); assert_tokens(&sk.readable(), &[Token::Str(SK_STR)]); assert_tokens(&sk.readable(), &[Token::String(SK_STR)]); - assert_tokens(&pk.compact(), &[Token::BorrowedBytes(&PK_BYTES[..])]); - assert_tokens(&pk.compact(), &[Token::Bytes(&PK_BYTES)]); - assert_tokens(&pk.compact(), &[Token::ByteBuf(&PK_BYTES)]); + assert_tokens(&pk.compact(), &[ + Token::Tuple{ len: 33 }, + Token::U8(0x02), + Token::U8(0x18), Token::U8(0x84), Token::U8(0x57), Token::U8(0x81), Token::U8(0xf6), Token::U8(0x31), Token::U8(0xc4), Token::U8(0x8f), + Token::U8(0x1c), Token::U8(0x97), Token::U8(0x09), Token::U8(0xe2), Token::U8(0x30), Token::U8(0x92), Token::U8(0x06), Token::U8(0x7d), + Token::U8(0x06), Token::U8(0x83), Token::U8(0x7f), Token::U8(0x30), Token::U8(0xaa), Token::U8(0x0c), Token::U8(0xd0), Token::U8(0x54), + Token::U8(0x4a), Token::U8(0xc8), Token::U8(0x87), Token::U8(0xfe), Token::U8(0x91), Token::U8(0xdd), Token::U8(0xd1), Token::U8(0x66), + Token::TupleEnd + ]); assert_tokens(&pk.readable(), &[Token::BorrowedStr(PK_STR)]); assert_tokens(&pk.readable(), &[Token::Str(PK_STR)]); assert_tokens(&pk.readable(), &[Token::String(PK_STR)]); - } #[test] @@ -2027,12 +2059,52 @@ mod test { let sk = KeyPairWrapper(KeyPair::from_seckey_slice(&::SECP256K1, &SK_BYTES).unwrap()); - assert_tokens(&sk.compact(), &[Token::BorrowedBytes(&SK_BYTES[..])]); - assert_tokens(&sk.compact(), &[Token::Bytes(&SK_BYTES)]); - assert_tokens(&sk.compact(), &[Token::ByteBuf(&SK_BYTES)]); + assert_tokens(&sk.compact(), &[ + Token::Tuple{ len: 32 }, + Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), Token::U8(1), + Token::U8(0), Token::U8(1), Token::U8(2), Token::U8(3), Token::U8(4), Token::U8(5), Token::U8(6), Token::U8(7), + Token::U8(0xff), Token::U8(0xff), Token::U8(0), Token::U8(0), Token::U8(0xff), Token::U8(0xff), Token::U8(0), Token::U8(0), + Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), Token::U8(99), + Token::TupleEnd + ]); assert_tokens(&sk.readable(), &[Token::BorrowedStr(SK_STR)]); assert_tokens(&sk.readable(), &[Token::Str(SK_STR)]); assert_tokens(&sk.readable(), &[Token::String(SK_STR)]); } + + #[test] + #[cfg(not(fuzzing))] + #[cfg(all(feature = "global-context", feature = "serde"))] + fn test_serde_x_only_pubkey() { + use serde_test::{Configure, Token, assert_tokens}; + use key::KeyPair; + + static SK_BYTES: [u8; 32] = [ + 1, 1, 1, 1, 1, 1, 1, 1, + 0, 1, 2, 3, 4, 5, 6, 7, + 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + + static PK_STR: &'static str = "\ + 18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166\ + "; + + let kp = KeyPair::from_seckey_slice(&::SECP256K1, &SK_BYTES).unwrap(); + let pk = XOnlyPublicKey::from_keypair(&kp); + + assert_tokens(&pk.compact(), &[ + Token::Tuple{ len: 32 }, + Token::U8(0x18), Token::U8(0x84), Token::U8(0x57), Token::U8(0x81), Token::U8(0xf6), Token::U8(0x31), Token::U8(0xc4), Token::U8(0x8f), + Token::U8(0x1c), Token::U8(0x97), Token::U8(0x09), Token::U8(0xe2), Token::U8(0x30), Token::U8(0x92), Token::U8(0x06), Token::U8(0x7d), + Token::U8(0x06), Token::U8(0x83), Token::U8(0x7f), Token::U8(0x30), Token::U8(0xaa), Token::U8(0x0c), Token::U8(0xd0), Token::U8(0x54), + Token::U8(0x4a), Token::U8(0xc8), Token::U8(0x87), Token::U8(0xfe), Token::U8(0x91), Token::U8(0xdd), Token::U8(0xd1), Token::U8(0x66), + Token::TupleEnd + ]); + + assert_tokens(&pk.readable(), &[Token::BorrowedStr(PK_STR)]); + assert_tokens(&pk.readable(), &[Token::Str(PK_STR)]); + assert_tokens(&pk.readable(), &[Token::String(PK_STR)]); + } } diff --git a/src/lib.rs b/src/lib.rs index 93e1c1ab4..116bb7cd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,9 @@ #![cfg_attr(all(test, feature = "unstable"), feature(test))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(all(feature = "serde", not(feature = "std"), not(feature = "alloc")))] +compile_error!("Feature serde requires an allocator, please enable 'std' or 'alloc'"); + #[macro_use] pub extern crate secp256k1_sys; pub use secp256k1_sys as ffi; diff --git a/src/schnorr.rs b/src/schnorr.rs index a66b71e1b..d72dd994c 100644 --- a/src/schnorr.rs +++ b/src/schnorr.rs @@ -595,9 +595,14 @@ mod tests { assert_tokens(&sig.readable(), &[Token::Str(SIG_STR)]); assert_tokens(&sig.readable(), &[Token::String(SIG_STR)]); - assert_tokens(&pk.compact(), &[Token::BorrowedBytes(&PK_BYTES[..])]); - assert_tokens(&pk.compact(), &[Token::Bytes(&PK_BYTES[..])]); - assert_tokens(&pk.compact(), &[Token::ByteBuf(&PK_BYTES[..])]); + assert_tokens(&pk.compact(), &[ + Token::Tuple{ len: 32 }, + Token::U8(24), Token::U8(132), Token::U8(87), Token::U8(129), Token::U8(246), Token::U8(49), Token::U8(196), Token::U8(143), + Token::U8(28), Token::U8(151), Token::U8(9), Token::U8(226), Token::U8(48), Token::U8(146), Token::U8(6), Token::U8(125), + Token::U8(6), Token::U8(131), Token::U8(127), Token::U8(48), Token::U8(170), Token::U8(12), Token::U8(208), Token::U8(84), + Token::U8(74), Token::U8(200), Token::U8(135), Token::U8(254), Token::U8(145), Token::U8(221), Token::U8(209), Token::U8(102), + Token::TupleEnd + ]); assert_tokens(&pk.readable(), &[Token::BorrowedStr(PK_STR)]); assert_tokens(&pk.readable(), &[Token::Str(PK_STR)]); diff --git a/src/serde_util.rs b/src/serde_util.rs index bc815bc5e..3f7d6d334 100644 --- a/src/serde_util.rs +++ b/src/serde_util.rs @@ -67,3 +67,57 @@ where (self.parse_fn)(v).map_err(E::custom) } } + +#[cfg(any(feature = "std", feature = "alloc"))] +pub struct SeqVisitor { + expectation: &'static str, + parse_fn: F, +} + +#[cfg(any(feature = "std", feature = "alloc"))] +impl SeqVisitor +where + F: FnOnce(&[u8]) -> Result, + Err: fmt::Display, +{ + pub fn new(expectation: &'static str, parse_fn: F) -> Self { + SeqVisitor { + expectation, + parse_fn, + } + } +} + +#[cfg(any(feature = "std", feature = "alloc"))] +impl<'de, F, T, Err> de::Visitor<'de> for SeqVisitor +where + F: FnOnce(&[u8]) -> Result, + Err: fmt::Display, +{ + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(self.expectation) + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: de::SeqAccess<'de>, + { + #[cfg(all(not(feature = "std"), feature = "alloc"))] + use alloc; + + #[cfg(all(not(feature = "std"), feature = "alloc"))] + type Vec = alloc::vec::Vec; + + let mut v = match seq.size_hint() { + Some(size) => Vec::with_capacity(size), + None => Vec::new(), + }; + + while let Some(value) = seq.next_element()? { + v.push(value) + } + (self.parse_fn)(&v).map_err(de::Error::custom) + } +}